huggingface-hub 0.32.2__py3-none-any.whl → 0.32.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of huggingface-hub might be problematic. Click here for more details.

@@ -46,7 +46,7 @@ import sys
46
46
  from typing import TYPE_CHECKING
47
47
 
48
48
 
49
- __version__ = "0.32.2"
49
+ __version__ = "0.32.3"
50
50
 
51
51
  # Alphabetical order of definitions is ensured in tests
52
52
  # WARNING: any comment added in this dictionary definition will be lost when
@@ -0,0 +1,87 @@
1
+ import asyncio
2
+ import sys
3
+ from functools import partial
4
+
5
+ import typer
6
+
7
+
8
+ def _patch_anyio_open_process():
9
+ """
10
+ Patch anyio.open_process to allow detached processes on Windows and Unix-like systems.
11
+
12
+ This is necessary to prevent the MCP client from being interrupted by Ctrl+C when running in the CLI.
13
+ """
14
+ import subprocess
15
+
16
+ import anyio
17
+
18
+ if getattr(anyio, "_tiny_agents_patched", False):
19
+ return
20
+ anyio._tiny_agents_patched = True
21
+
22
+ original_open_process = anyio.open_process
23
+
24
+ if sys.platform == "win32":
25
+ # On Windows, we need to set the creation flags to create a new process group
26
+
27
+ async def open_process_in_new_group(*args, **kwargs):
28
+ """
29
+ Wrapper for open_process to handle Windows-specific process creation flags.
30
+ """
31
+ # Ensure we pass the creation flags for Windows
32
+ kwargs.setdefault("creationflags", subprocess.CREATE_NEW_PROCESS_GROUP)
33
+ return await original_open_process(*args, **kwargs)
34
+
35
+ anyio.open_process = open_process_in_new_group
36
+ else:
37
+ # For Unix-like systems, we can use setsid to create a new session
38
+ async def open_process_in_new_group(*args, **kwargs):
39
+ """
40
+ Wrapper for open_process to handle Unix-like systems with start_new_session=True.
41
+ """
42
+ kwargs.setdefault("start_new_session", True)
43
+ return await original_open_process(*args, **kwargs)
44
+
45
+ anyio.open_process = open_process_in_new_group
46
+
47
+
48
+ async def _async_prompt(exit_event: asyncio.Event, prompt: str = "» ") -> str:
49
+ """
50
+ Asynchronous prompt function that reads input from stdin without blocking.
51
+
52
+ This function is designed to work in an asynchronous context, allowing the event loop to gracefully stop it (e.g. on Ctrl+C).
53
+
54
+ Alternatively, we could use https://github.com/vxgmichel/aioconsole but that would be an additional dependency.
55
+ """
56
+ loop = asyncio.get_event_loop()
57
+
58
+ if sys.platform == "win32":
59
+ # Windows: Use run_in_executor to avoid blocking the event loop
60
+ # Degraded solution: this is not ideal as user will have to CTRL+C once more to stop the prompt (and it'll not be graceful)
61
+ return await loop.run_in_executor(None, partial(typer.prompt, prompt, prompt_suffix=" "))
62
+ else:
63
+ # UNIX-like: Use loop.add_reader for non-blocking stdin read
64
+ future = loop.create_future()
65
+
66
+ def on_input():
67
+ line = sys.stdin.readline()
68
+ loop.remove_reader(sys.stdin)
69
+ future.set_result(line)
70
+
71
+ print(prompt, end=" ", flush=True)
72
+ loop.add_reader(sys.stdin, on_input) # not supported on Windows
73
+
74
+ # Wait for user input or exit event
75
+ # Wait until either the user hits enter or exit_event is set
76
+ await asyncio.wait(
77
+ [future, exit_event.wait()],
78
+ return_when=asyncio.FIRST_COMPLETED,
79
+ )
80
+
81
+ # Check which one has been triggered
82
+ if exit_event.is_set():
83
+ future.cancel()
84
+ return ""
85
+
86
+ line = await future
87
+ return line.strip()
@@ -2,12 +2,12 @@ import asyncio
2
2
  import os
3
3
  import signal
4
4
  import traceback
5
- from functools import partial
6
5
  from typing import Any, Dict, List, Optional
7
6
 
8
7
  import typer
9
8
  from rich import print
10
9
 
10
+ from ._cli_hacks import _async_prompt, _patch_anyio_open_process
11
11
  from .agent import Agent
12
12
  from .utils import _load_agent_config
13
13
 
@@ -25,11 +25,6 @@ run_cli = typer.Typer(
25
25
  app.add_typer(run_cli, name="run")
26
26
 
27
27
 
28
- async def _ainput(prompt: str = "» ") -> str:
29
- loop = asyncio.get_running_loop()
30
- return await loop.run_in_executor(None, partial(typer.prompt, prompt, prompt_suffix=" "))
31
-
32
-
33
28
  async def run_agent(
34
29
  agent_path: Optional[str],
35
30
  ) -> None:
@@ -41,11 +36,15 @@ async def run_agent(
41
36
  Path to a local folder containing an `agent.json` and optionally a custom `PROMPT.md` file or a built-in agent stored in a Hugging Face dataset.
42
37
 
43
38
  """
39
+ _patch_anyio_open_process() # Hacky way to prevent stdio connections to be stopped by Ctrl+C
40
+
44
41
  config, prompt = _load_agent_config(agent_path)
45
42
 
43
+ inputs: List[Dict[str, Any]] = config.get("inputs", [])
46
44
  servers: List[Dict[str, Any]] = config.get("servers", [])
47
45
 
48
46
  abort_event = asyncio.Event()
47
+ exit_event = asyncio.Event()
49
48
  first_sigint = True
50
49
 
51
50
  loop = asyncio.get_running_loop()
@@ -60,8 +59,7 @@ async def run_agent(
60
59
  return
61
60
 
62
61
  print("\n[red]Exiting...[/red]", flush=True)
63
-
64
- os._exit(130)
62
+ exit_event.set()
65
63
 
66
64
  try:
67
65
  sigint_registered_in_loop = False
@@ -71,6 +69,61 @@ async def run_agent(
71
69
  except (AttributeError, NotImplementedError):
72
70
  # Windows (or any loop that doesn't support it) : fall back to sync
73
71
  signal.signal(signal.SIGINT, lambda *_: _sigint_handler())
72
+
73
+ # Handle inputs (i.e. env variables injection)
74
+ if len(inputs) > 0:
75
+ print(
76
+ "[bold blue]Some initial inputs are required by the agent. "
77
+ "Please provide a value or leave empty to load from env.[/bold blue]"
78
+ )
79
+ for input_item in inputs:
80
+ input_id = input_item["id"]
81
+ description = input_item["description"]
82
+ env_special_value = "${input:" + input_id + "}" # Special value to indicate env variable injection
83
+
84
+ # Check env variables that will use this input
85
+ input_vars = list(
86
+ {
87
+ key
88
+ for server in servers
89
+ for key, value in server.get("config", {}).get("env", {}).items()
90
+ if value == env_special_value
91
+ }
92
+ )
93
+
94
+ if not input_vars:
95
+ print(f"[yellow]Input {input_id} defined in config but not used by any server.[/yellow]")
96
+ continue
97
+
98
+ # Prompt user for input
99
+ print(
100
+ f"[blue] • {input_id}[/blue]: {description}. (default: load from {', '.join(input_vars)}).",
101
+ end=" ",
102
+ )
103
+ user_input = (await _async_prompt(exit_event=exit_event)).strip()
104
+ if exit_event.is_set():
105
+ return
106
+
107
+ # Inject user input (or env variable) into servers' env
108
+ for server in servers:
109
+ env = server.get("config", {}).get("env", {})
110
+ for key, value in env.items():
111
+ if value == env_special_value:
112
+ if user_input:
113
+ env[key] = user_input
114
+ else:
115
+ value_from_env = os.getenv(key, "")
116
+ env[key] = value_from_env
117
+ if value_from_env:
118
+ print(f"[green]Value successfully loaded from '{key}'[/green]")
119
+ else:
120
+ print(
121
+ f"[yellow]No value found for '{key}' in environment variables. Continuing.[/yellow]"
122
+ )
123
+
124
+ print()
125
+
126
+ # Main agent loop
74
127
  async with Agent(
75
128
  provider=config.get("provider"),
76
129
  model=config.get("model"),
@@ -86,8 +139,12 @@ async def run_agent(
86
139
  while True:
87
140
  abort_event.clear()
88
141
 
142
+ # Check if we should exit
143
+ if exit_event.is_set():
144
+ return
145
+
89
146
  try:
90
- user_input = await _ainput()
147
+ user_input = await _async_prompt(exit_event=exit_event)
91
148
  first_sigint = True
92
149
  except EOFError:
93
150
  print("\n[red]EOF received, exiting.[/red]", flush=True)
@@ -103,6 +160,8 @@ async def run_agent(
103
160
  async for chunk in agent.run(user_input, abort_event=abort_event):
104
161
  if abort_event.is_set() and not first_sigint:
105
162
  break
163
+ if exit_event.is_set():
164
+ return
106
165
 
107
166
  if hasattr(chunk, "choices"):
108
167
  delta = chunk.choices[0].delta
@@ -109,6 +109,11 @@ class MCPClient:
109
109
  await self.client.__aexit__(exc_type, exc_val, exc_tb)
110
110
  await self.cleanup()
111
111
 
112
+ async def cleanup(self):
113
+ """Clean up resources"""
114
+ await self.client.close()
115
+ await self.exit_stack.aclose()
116
+
112
117
  @overload
113
118
  async def add_mcp_server(self, type: Literal["stdio"], **params: Unpack[StdioServerParameters_T]): ...
114
119
 
@@ -286,11 +291,13 @@ class MCPClient:
286
291
  for tool_call in delta.tool_calls:
287
292
  # Aggregate chunks into tool calls
288
293
  if tool_call.index not in final_tool_calls:
289
- if tool_call.function.arguments is None: # Corner case (depends on provider)
294
+ if (
295
+ tool_call.function.arguments is None or tool_call.function.arguments == "{}"
296
+ ): # Corner case (depends on provider)
290
297
  tool_call.function.arguments = ""
291
298
  final_tool_calls[tool_call.index] = tool_call
292
299
 
293
- if tool_call.function.arguments:
300
+ elif tool_call.function.arguments:
294
301
  final_tool_calls[tool_call.index].function.arguments += tool_call.function.arguments
295
302
 
296
303
  # Optionally exit early if no tools in first chunks
@@ -327,7 +334,3 @@ class MCPClient:
327
334
  tool_message_as_obj = ChatCompletionInputMessage.parse_obj_as_instance(tool_message)
328
335
  messages.append(tool_message_as_obj)
329
336
  yield tool_message_as_obj
330
-
331
- async def cleanup(self):
332
- """Clean up resources"""
333
- await self.exit_stack.aclose()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: huggingface-hub
3
- Version: 0.32.2
3
+ Version: 0.32.3
4
4
  Summary: Client library to download and publish models, datasets and other repos on the huggingface.co hub
5
5
  Home-page: https://github.com/huggingface/huggingface_hub
6
6
  Author: Hugging Face, Inc.
@@ -1,4 +1,4 @@
1
- huggingface_hub/__init__.py,sha256=j0CPc82aFQA-FV3ew00Ukp6u4v-Lz3cBrH-K33Z50yE,50644
1
+ huggingface_hub/__init__.py,sha256=cK2MmEMaR4mvhSeDsQFuY2MLlfw8A3lKZbrwTzs-V9E,50644
2
2
  huggingface_hub/_commit_api.py,sha256=ZbmuIhFdF8B3F_cvGtxorka7MmIQOk8oBkCtYltnCvI,39456
3
3
  huggingface_hub/_commit_scheduler.py,sha256=tfIoO1xWHjTJ6qy6VS6HIoymDycFPg0d6pBSZprrU2U,14679
4
4
  huggingface_hub/_inference_endpoints.py,sha256=qXR0utAYRaEWTI8EXzAsDpVDcYpp8bJPEBbcOxRS52E,17413
@@ -81,10 +81,11 @@ huggingface_hub/inference/_generated/types/zero_shot_classification.py,sha256=BA
81
81
  huggingface_hub/inference/_generated/types/zero_shot_image_classification.py,sha256=8J9n6VqFARkWvPfAZNWEG70AlrMGldU95EGQQwn06zI,1487
82
82
  huggingface_hub/inference/_generated/types/zero_shot_object_detection.py,sha256=GUd81LIV7oEbRWayDlAVgyLmY596r1M3AW0jXDp1yTA,1630
83
83
  huggingface_hub/inference/_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
+ huggingface_hub/inference/_mcp/_cli_hacks.py,sha256=NP8xA-7-3kJIzLHg3FuHXdnpli89OAMPILPrTk60lwU,3131
84
85
  huggingface_hub/inference/_mcp/agent.py,sha256=azX9_lsFjNlgsEvRYdKgsmOmpNReWIcbuMeIVWc852k,4264
85
- huggingface_hub/inference/_mcp/cli.py,sha256=q7WUlc7K9cHffKnS8uVydqM7IGxDz2gnwoNKkiRF5GU,5873
86
+ huggingface_hub/inference/_mcp/cli.py,sha256=6WP2bqdfSj80IYdwnzwO4ilEb7PgghgctD4iLV4ydqg,8588
86
87
  huggingface_hub/inference/_mcp/constants.py,sha256=tE_V6qcvsmvVoJa4eg04jhoTR2Cx1cNHieY2ENrm1_M,2511
87
- huggingface_hub/inference/_mcp/mcp_client.py,sha256=HUDkEURrWeQ9AY7W4_H4jca0BM12rKPbjdP4vNaXiwc,13998
88
+ huggingface_hub/inference/_mcp/mcp_client.py,sha256=yHfpfztIepARqD_3bFSFWOn402BWO1tptqlIVGR7zJk,14130
88
89
  huggingface_hub/inference/_mcp/utils.py,sha256=K7rr4FxCh9OYWwYNlnvQraNLy9y3z-5yVMBIaoCQMjA,4052
89
90
  huggingface_hub/inference/_providers/__init__.py,sha256=IrLTMERrbRuPiVdBQEMK9TMvXrsGId4-u2ucMkG-vTU,7671
90
91
  huggingface_hub/inference/_providers/_common.py,sha256=Octgz-PbHw62iW3Oa8rF7rxvBJR0ZmL4ouv3NoX-weE,10131
@@ -137,9 +138,9 @@ huggingface_hub/utils/insecure_hashlib.py,sha256=iAaepavFZ5Dhfa5n8KozRfQprKmvcjS
137
138
  huggingface_hub/utils/logging.py,sha256=0A8fF1yh3L9Ka_bCDX2ml4U5Ht0tY8Dr3JcbRvWFuwo,4909
138
139
  huggingface_hub/utils/sha.py,sha256=OFnNGCba0sNcT2gUwaVCJnldxlltrHHe0DS_PCpV3C4,2134
139
140
  huggingface_hub/utils/tqdm.py,sha256=xAKcyfnNHsZ7L09WuEM5Ew5-MDhiahLACbbN2zMmcLs,10671
140
- huggingface_hub-0.32.2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
141
- huggingface_hub-0.32.2.dist-info/METADATA,sha256=w2Df9xLc5FhDmwO11yIxMDiHc-i7aqLvXFRzRvzgI_E,14777
142
- huggingface_hub-0.32.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
143
- huggingface_hub-0.32.2.dist-info/entry_points.txt,sha256=uelw0-fu0kd-CxIuOsR1bsjLIFnAaMQ6AIqluJYDhQw,184
144
- huggingface_hub-0.32.2.dist-info/top_level.txt,sha256=8KzlQJAY4miUvjAssOAJodqKOw3harNzuiwGQ9qLSSk,16
145
- huggingface_hub-0.32.2.dist-info/RECORD,,
141
+ huggingface_hub-0.32.3.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
142
+ huggingface_hub-0.32.3.dist-info/METADATA,sha256=YG4e05qfvJNzzMc6QTzFoDez-KfYpn3acXE_LmzpllY,14777
143
+ huggingface_hub-0.32.3.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
144
+ huggingface_hub-0.32.3.dist-info/entry_points.txt,sha256=uelw0-fu0kd-CxIuOsR1bsjLIFnAaMQ6AIqluJYDhQw,184
145
+ huggingface_hub-0.32.3.dist-info/top_level.txt,sha256=8KzlQJAY4miUvjAssOAJodqKOw3harNzuiwGQ9qLSSk,16
146
+ huggingface_hub-0.32.3.dist-info/RECORD,,