huggingface-hub 0.29.0rc2__py3-none-any.whl → 1.1.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.
Files changed (153) hide show
  1. huggingface_hub/__init__.py +160 -46
  2. huggingface_hub/_commit_api.py +277 -71
  3. huggingface_hub/_commit_scheduler.py +15 -15
  4. huggingface_hub/_inference_endpoints.py +33 -22
  5. huggingface_hub/_jobs_api.py +301 -0
  6. huggingface_hub/_local_folder.py +18 -3
  7. huggingface_hub/_login.py +31 -63
  8. huggingface_hub/_oauth.py +460 -0
  9. huggingface_hub/_snapshot_download.py +241 -81
  10. huggingface_hub/_space_api.py +18 -10
  11. huggingface_hub/_tensorboard_logger.py +15 -19
  12. huggingface_hub/_upload_large_folder.py +196 -76
  13. huggingface_hub/_webhooks_payload.py +3 -3
  14. huggingface_hub/_webhooks_server.py +15 -25
  15. huggingface_hub/{commands → cli}/__init__.py +1 -15
  16. huggingface_hub/cli/_cli_utils.py +173 -0
  17. huggingface_hub/cli/auth.py +147 -0
  18. huggingface_hub/cli/cache.py +841 -0
  19. huggingface_hub/cli/download.py +189 -0
  20. huggingface_hub/cli/hf.py +60 -0
  21. huggingface_hub/cli/inference_endpoints.py +377 -0
  22. huggingface_hub/cli/jobs.py +772 -0
  23. huggingface_hub/cli/lfs.py +175 -0
  24. huggingface_hub/cli/repo.py +315 -0
  25. huggingface_hub/cli/repo_files.py +94 -0
  26. huggingface_hub/{commands/env.py → cli/system.py} +10 -13
  27. huggingface_hub/cli/upload.py +294 -0
  28. huggingface_hub/cli/upload_large_folder.py +117 -0
  29. huggingface_hub/community.py +20 -12
  30. huggingface_hub/constants.py +83 -59
  31. huggingface_hub/dataclasses.py +609 -0
  32. huggingface_hub/errors.py +99 -30
  33. huggingface_hub/fastai_utils.py +30 -41
  34. huggingface_hub/file_download.py +606 -346
  35. huggingface_hub/hf_api.py +2445 -1132
  36. huggingface_hub/hf_file_system.py +269 -152
  37. huggingface_hub/hub_mixin.py +61 -66
  38. huggingface_hub/inference/_client.py +501 -630
  39. huggingface_hub/inference/_common.py +133 -121
  40. huggingface_hub/inference/_generated/_async_client.py +536 -722
  41. huggingface_hub/inference/_generated/types/__init__.py +6 -1
  42. huggingface_hub/inference/_generated/types/automatic_speech_recognition.py +5 -6
  43. huggingface_hub/inference/_generated/types/base.py +10 -7
  44. huggingface_hub/inference/_generated/types/chat_completion.py +77 -31
  45. huggingface_hub/inference/_generated/types/depth_estimation.py +2 -2
  46. huggingface_hub/inference/_generated/types/document_question_answering.py +2 -2
  47. huggingface_hub/inference/_generated/types/feature_extraction.py +2 -2
  48. huggingface_hub/inference/_generated/types/fill_mask.py +2 -2
  49. huggingface_hub/inference/_generated/types/image_to_image.py +8 -2
  50. huggingface_hub/inference/_generated/types/image_to_text.py +2 -3
  51. huggingface_hub/inference/_generated/types/image_to_video.py +60 -0
  52. huggingface_hub/inference/_generated/types/sentence_similarity.py +3 -3
  53. huggingface_hub/inference/_generated/types/summarization.py +2 -2
  54. huggingface_hub/inference/_generated/types/table_question_answering.py +5 -5
  55. huggingface_hub/inference/_generated/types/text2text_generation.py +2 -2
  56. huggingface_hub/inference/_generated/types/text_generation.py +11 -11
  57. huggingface_hub/inference/_generated/types/text_to_audio.py +1 -2
  58. huggingface_hub/inference/_generated/types/text_to_speech.py +1 -2
  59. huggingface_hub/inference/_generated/types/text_to_video.py +2 -2
  60. huggingface_hub/inference/_generated/types/token_classification.py +2 -2
  61. huggingface_hub/inference/_generated/types/translation.py +2 -2
  62. huggingface_hub/inference/_generated/types/zero_shot_classification.py +2 -2
  63. huggingface_hub/inference/_generated/types/zero_shot_image_classification.py +2 -2
  64. huggingface_hub/inference/_generated/types/zero_shot_object_detection.py +1 -3
  65. huggingface_hub/inference/_mcp/__init__.py +0 -0
  66. huggingface_hub/inference/_mcp/_cli_hacks.py +88 -0
  67. huggingface_hub/inference/_mcp/agent.py +100 -0
  68. huggingface_hub/inference/_mcp/cli.py +247 -0
  69. huggingface_hub/inference/_mcp/constants.py +81 -0
  70. huggingface_hub/inference/_mcp/mcp_client.py +395 -0
  71. huggingface_hub/inference/_mcp/types.py +45 -0
  72. huggingface_hub/inference/_mcp/utils.py +128 -0
  73. huggingface_hub/inference/_providers/__init__.py +149 -20
  74. huggingface_hub/inference/_providers/_common.py +160 -37
  75. huggingface_hub/inference/_providers/black_forest_labs.py +12 -9
  76. huggingface_hub/inference/_providers/cerebras.py +6 -0
  77. huggingface_hub/inference/_providers/clarifai.py +13 -0
  78. huggingface_hub/inference/_providers/cohere.py +32 -0
  79. huggingface_hub/inference/_providers/fal_ai.py +231 -22
  80. huggingface_hub/inference/_providers/featherless_ai.py +38 -0
  81. huggingface_hub/inference/_providers/fireworks_ai.py +22 -1
  82. huggingface_hub/inference/_providers/groq.py +9 -0
  83. huggingface_hub/inference/_providers/hf_inference.py +143 -33
  84. huggingface_hub/inference/_providers/hyperbolic.py +9 -5
  85. huggingface_hub/inference/_providers/nebius.py +47 -5
  86. huggingface_hub/inference/_providers/novita.py +48 -5
  87. huggingface_hub/inference/_providers/nscale.py +44 -0
  88. huggingface_hub/inference/_providers/openai.py +25 -0
  89. huggingface_hub/inference/_providers/publicai.py +6 -0
  90. huggingface_hub/inference/_providers/replicate.py +46 -9
  91. huggingface_hub/inference/_providers/sambanova.py +37 -1
  92. huggingface_hub/inference/_providers/scaleway.py +28 -0
  93. huggingface_hub/inference/_providers/together.py +34 -5
  94. huggingface_hub/inference/_providers/wavespeed.py +138 -0
  95. huggingface_hub/inference/_providers/zai_org.py +17 -0
  96. huggingface_hub/lfs.py +33 -100
  97. huggingface_hub/repocard.py +34 -38
  98. huggingface_hub/repocard_data.py +79 -59
  99. huggingface_hub/serialization/__init__.py +0 -1
  100. huggingface_hub/serialization/_base.py +12 -15
  101. huggingface_hub/serialization/_dduf.py +8 -8
  102. huggingface_hub/serialization/_torch.py +69 -69
  103. huggingface_hub/utils/__init__.py +27 -8
  104. huggingface_hub/utils/_auth.py +7 -7
  105. huggingface_hub/utils/_cache_manager.py +92 -147
  106. huggingface_hub/utils/_chunk_utils.py +2 -3
  107. huggingface_hub/utils/_deprecation.py +1 -1
  108. huggingface_hub/utils/_dotenv.py +55 -0
  109. huggingface_hub/utils/_experimental.py +7 -5
  110. huggingface_hub/utils/_fixes.py +0 -10
  111. huggingface_hub/utils/_git_credential.py +5 -5
  112. huggingface_hub/utils/_headers.py +8 -30
  113. huggingface_hub/utils/_http.py +399 -237
  114. huggingface_hub/utils/_pagination.py +6 -6
  115. huggingface_hub/utils/_parsing.py +98 -0
  116. huggingface_hub/utils/_paths.py +5 -5
  117. huggingface_hub/utils/_runtime.py +74 -22
  118. huggingface_hub/utils/_safetensors.py +21 -21
  119. huggingface_hub/utils/_subprocess.py +13 -11
  120. huggingface_hub/utils/_telemetry.py +4 -4
  121. huggingface_hub/{commands/_cli_utils.py → utils/_terminal.py} +4 -4
  122. huggingface_hub/utils/_typing.py +25 -5
  123. huggingface_hub/utils/_validators.py +55 -74
  124. huggingface_hub/utils/_verification.py +167 -0
  125. huggingface_hub/utils/_xet.py +235 -0
  126. huggingface_hub/utils/_xet_progress_reporting.py +162 -0
  127. huggingface_hub/utils/insecure_hashlib.py +3 -5
  128. huggingface_hub/utils/logging.py +8 -11
  129. huggingface_hub/utils/tqdm.py +33 -4
  130. {huggingface_hub-0.29.0rc2.dist-info → huggingface_hub-1.1.3.dist-info}/METADATA +94 -82
  131. huggingface_hub-1.1.3.dist-info/RECORD +155 -0
  132. {huggingface_hub-0.29.0rc2.dist-info → huggingface_hub-1.1.3.dist-info}/WHEEL +1 -1
  133. huggingface_hub-1.1.3.dist-info/entry_points.txt +6 -0
  134. huggingface_hub/commands/delete_cache.py +0 -428
  135. huggingface_hub/commands/download.py +0 -200
  136. huggingface_hub/commands/huggingface_cli.py +0 -61
  137. huggingface_hub/commands/lfs.py +0 -200
  138. huggingface_hub/commands/repo_files.py +0 -128
  139. huggingface_hub/commands/scan_cache.py +0 -181
  140. huggingface_hub/commands/tag.py +0 -159
  141. huggingface_hub/commands/upload.py +0 -299
  142. huggingface_hub/commands/upload_large_folder.py +0 -129
  143. huggingface_hub/commands/user.py +0 -304
  144. huggingface_hub/commands/version.py +0 -37
  145. huggingface_hub/inference_api.py +0 -217
  146. huggingface_hub/keras_mixin.py +0 -500
  147. huggingface_hub/repository.py +0 -1477
  148. huggingface_hub/serialization/_tensorflow.py +0 -95
  149. huggingface_hub/utils/_hf_folder.py +0 -68
  150. huggingface_hub-0.29.0rc2.dist-info/RECORD +0 -131
  151. huggingface_hub-0.29.0rc2.dist-info/entry_points.txt +0 -6
  152. {huggingface_hub-0.29.0rc2.dist-info → huggingface_hub-1.1.3.dist-info/licenses}/LICENSE +0 -0
  153. {huggingface_hub-0.29.0rc2.dist-info → huggingface_hub-1.1.3.dist-info}/top_level.txt +0 -0
@@ -3,12 +3,12 @@
3
3
  # See:
4
4
  # - script: https://github.com/huggingface/huggingface.js/blob/main/packages/tasks/scripts/inference-codegen.ts
5
5
  # - specs: https://github.com/huggingface/huggingface.js/tree/main/packages/tasks/src/tasks.
6
- from typing import Any, List, Literal, Optional
6
+ from typing import Any, Literal, Optional
7
7
 
8
8
  from .base import BaseInferenceType, dataclass_with_extra
9
9
 
10
10
 
11
- TypeEnum = Literal["json", "regex"]
11
+ TypeEnum = Literal["json", "regex", "json_schema"]
12
12
 
13
13
 
14
14
  @dataclass_with_extra
@@ -49,7 +49,7 @@ class TextGenerationInputGenerateParameters(BaseInferenceType):
49
49
  """Whether to prepend the prompt to the generated text"""
50
50
  seed: Optional[int] = None
51
51
  """Random sampling seed."""
52
- stop: Optional[List[str]] = None
52
+ stop: Optional[list[str]] = None
53
53
  """Stop generating tokens if a member of `stop` is generated."""
54
54
  temperature: Optional[float] = None
55
55
  """The value used to module the logits distribution."""
@@ -108,21 +108,21 @@ class TextGenerationOutputBestOfSequence(BaseInferenceType):
108
108
  finish_reason: "TextGenerationOutputFinishReason"
109
109
  generated_text: str
110
110
  generated_tokens: int
111
- prefill: List[TextGenerationOutputPrefillToken]
112
- tokens: List[TextGenerationOutputToken]
111
+ prefill: list[TextGenerationOutputPrefillToken]
112
+ tokens: list[TextGenerationOutputToken]
113
113
  seed: Optional[int] = None
114
- top_tokens: Optional[List[List[TextGenerationOutputToken]]] = None
114
+ top_tokens: Optional[list[list[TextGenerationOutputToken]]] = None
115
115
 
116
116
 
117
117
  @dataclass_with_extra
118
118
  class TextGenerationOutputDetails(BaseInferenceType):
119
119
  finish_reason: "TextGenerationOutputFinishReason"
120
120
  generated_tokens: int
121
- prefill: List[TextGenerationOutputPrefillToken]
122
- tokens: List[TextGenerationOutputToken]
123
- best_of_sequences: Optional[List[TextGenerationOutputBestOfSequence]] = None
121
+ prefill: list[TextGenerationOutputPrefillToken]
122
+ tokens: list[TextGenerationOutputToken]
123
+ best_of_sequences: Optional[list[TextGenerationOutputBestOfSequence]] = None
124
124
  seed: Optional[int] = None
125
- top_tokens: Optional[List[List[TextGenerationOutputToken]]] = None
125
+ top_tokens: Optional[list[list[TextGenerationOutputToken]]] = None
126
126
 
127
127
 
128
128
  @dataclass_with_extra
@@ -165,4 +165,4 @@ class TextGenerationStreamOutput(BaseInferenceType):
165
165
  token: TextGenerationStreamOutputToken
166
166
  details: Optional[TextGenerationStreamOutputStreamDetails] = None
167
167
  generated_text: Optional[str] = None
168
- top_tokens: Optional[List[TextGenerationStreamOutputToken]] = None
168
+ top_tokens: Optional[list[TextGenerationStreamOutputToken]] = None
@@ -75,8 +75,7 @@ class TextToAudioGenerationParameters(BaseInferenceType):
75
75
  class TextToAudioParameters(BaseInferenceType):
76
76
  """Additional inference parameters for Text To Audio"""
77
77
 
78
- # Will be deprecated in the future when the renaming to `generation_parameters` is implemented in transformers
79
- generate_kwargs: Optional[TextToAudioGenerationParameters] = None
78
+ generation_parameters: Optional[TextToAudioGenerationParameters] = None
80
79
  """Parametrization of the text generation process"""
81
80
 
82
81
 
@@ -75,8 +75,7 @@ class TextToSpeechGenerationParameters(BaseInferenceType):
75
75
  class TextToSpeechParameters(BaseInferenceType):
76
76
  """Additional inference parameters for Text To Speech"""
77
77
 
78
- # Will be deprecated in the future when the renaming to `generation_parameters` is implemented in transformers
79
- generate_kwargs: Optional[TextToSpeechGenerationParameters] = None
78
+ generation_parameters: Optional[TextToSpeechGenerationParameters] = None
80
79
  """Parametrization of the text generation process"""
81
80
 
82
81
 
@@ -3,7 +3,7 @@
3
3
  # See:
4
4
  # - script: https://github.com/huggingface/huggingface.js/blob/main/packages/tasks/scripts/inference-codegen.ts
5
5
  # - specs: https://github.com/huggingface/huggingface.js/tree/main/packages/tasks/src/tasks.
6
- from typing import Any, List, Optional
6
+ from typing import Any, Optional
7
7
 
8
8
  from .base import BaseInferenceType, dataclass_with_extra
9
9
 
@@ -16,7 +16,7 @@ class TextToVideoParameters(BaseInferenceType):
16
16
  """A higher guidance scale value encourages the model to generate videos closely linked to
17
17
  the text prompt, but values too high may cause saturation and other artifacts.
18
18
  """
19
- negative_prompt: Optional[List[str]] = None
19
+ negative_prompt: Optional[list[str]] = None
20
20
  """One or several prompt to guide what NOT to include in video generation."""
21
21
  num_frames: Optional[float] = None
22
22
  """The num_frames parameter determines how many video frames are generated."""
@@ -3,7 +3,7 @@
3
3
  # See:
4
4
  # - script: https://github.com/huggingface/huggingface.js/blob/main/packages/tasks/scripts/inference-codegen.ts
5
5
  # - specs: https://github.com/huggingface/huggingface.js/tree/main/packages/tasks/src/tasks.
6
- from typing import List, Literal, Optional
6
+ from typing import Literal, Optional
7
7
 
8
8
  from .base import BaseInferenceType, dataclass_with_extra
9
9
 
@@ -17,7 +17,7 @@ class TokenClassificationParameters(BaseInferenceType):
17
17
 
18
18
  aggregation_strategy: Optional["TokenClassificationAggregationStrategy"] = None
19
19
  """The strategy used to fuse tokens based on model predictions"""
20
- ignore_labels: Optional[List[str]] = None
20
+ ignore_labels: Optional[list[str]] = None
21
21
  """A list of labels to ignore"""
22
22
  stride: Optional[int] = None
23
23
  """The number of overlapping tokens between chunks when splitting the input text."""
@@ -3,7 +3,7 @@
3
3
  # See:
4
4
  # - script: https://github.com/huggingface/huggingface.js/blob/main/packages/tasks/scripts/inference-codegen.ts
5
5
  # - specs: https://github.com/huggingface/huggingface.js/tree/main/packages/tasks/src/tasks.
6
- from typing import Any, Dict, Literal, Optional
6
+ from typing import Any, Literal, Optional
7
7
 
8
8
  from .base import BaseInferenceType, dataclass_with_extra
9
9
 
@@ -17,7 +17,7 @@ class TranslationParameters(BaseInferenceType):
17
17
 
18
18
  clean_up_tokenization_spaces: Optional[bool] = None
19
19
  """Whether to clean up the potential extra spaces in the text output."""
20
- generate_parameters: Optional[Dict[str, Any]] = None
20
+ generate_parameters: Optional[dict[str, Any]] = None
21
21
  """Additional parametrization of the text generation algorithm."""
22
22
  src_lang: Optional[str] = None
23
23
  """The source language of the text. Required for models that can translate from multiple
@@ -3,7 +3,7 @@
3
3
  # See:
4
4
  # - script: https://github.com/huggingface/huggingface.js/blob/main/packages/tasks/scripts/inference-codegen.ts
5
5
  # - specs: https://github.com/huggingface/huggingface.js/tree/main/packages/tasks/src/tasks.
6
- from typing import List, Optional
6
+ from typing import Optional
7
7
 
8
8
  from .base import BaseInferenceType, dataclass_with_extra
9
9
 
@@ -12,7 +12,7 @@ from .base import BaseInferenceType, dataclass_with_extra
12
12
  class ZeroShotClassificationParameters(BaseInferenceType):
13
13
  """Additional inference parameters for Zero Shot Classification"""
14
14
 
15
- candidate_labels: List[str]
15
+ candidate_labels: list[str]
16
16
  """The set of possible class labels to classify the text into."""
17
17
  hypothesis_template: Optional[str] = None
18
18
  """The sentence used in conjunction with `candidate_labels` to attempt the text
@@ -3,7 +3,7 @@
3
3
  # See:
4
4
  # - script: https://github.com/huggingface/huggingface.js/blob/main/packages/tasks/scripts/inference-codegen.ts
5
5
  # - specs: https://github.com/huggingface/huggingface.js/tree/main/packages/tasks/src/tasks.
6
- from typing import List, Optional
6
+ from typing import Optional
7
7
 
8
8
  from .base import BaseInferenceType, dataclass_with_extra
9
9
 
@@ -12,7 +12,7 @@ from .base import BaseInferenceType, dataclass_with_extra
12
12
  class ZeroShotImageClassificationParameters(BaseInferenceType):
13
13
  """Additional inference parameters for Zero Shot Image Classification"""
14
14
 
15
- candidate_labels: List[str]
15
+ candidate_labels: list[str]
16
16
  """The candidate labels for this image"""
17
17
  hypothesis_template: Optional[str] = None
18
18
  """The sentence used in conjunction with `candidate_labels` to attempt the image
@@ -3,8 +3,6 @@
3
3
  # See:
4
4
  # - script: https://github.com/huggingface/huggingface.js/blob/main/packages/tasks/scripts/inference-codegen.ts
5
5
  # - specs: https://github.com/huggingface/huggingface.js/tree/main/packages/tasks/src/tasks.
6
- from typing import List
7
-
8
6
  from .base import BaseInferenceType, dataclass_with_extra
9
7
 
10
8
 
@@ -12,7 +10,7 @@ from .base import BaseInferenceType, dataclass_with_extra
12
10
  class ZeroShotObjectDetectionParameters(BaseInferenceType):
13
11
  """Additional inference parameters for Zero Shot Object Detection"""
14
12
 
15
- candidate_labels: List[str]
13
+ candidate_labels: list[str]
16
14
  """The candidate labels for this image"""
17
15
 
18
16
 
File without changes
@@ -0,0 +1,88 @@
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 # ty: ignore[invalid-assignment]
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 # ty: ignore[invalid-assignment]
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 # ty: ignore[invalid-assignment]
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
+ exit_task = asyncio.create_task(exit_event.wait())
77
+ await asyncio.wait(
78
+ [future, exit_task],
79
+ return_when=asyncio.FIRST_COMPLETED,
80
+ )
81
+
82
+ # Check which one has been triggered
83
+ if exit_event.is_set():
84
+ future.cancel()
85
+ return ""
86
+
87
+ line = await future
88
+ return line.strip()
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import AsyncGenerator, Iterable, Optional, Union
5
+
6
+ from huggingface_hub import ChatCompletionInputMessage, ChatCompletionStreamOutput, MCPClient
7
+
8
+ from .._providers import PROVIDER_OR_POLICY_T
9
+ from .constants import DEFAULT_SYSTEM_PROMPT, EXIT_LOOP_TOOLS, MAX_NUM_TURNS
10
+ from .types import ServerConfig
11
+
12
+
13
+ class Agent(MCPClient):
14
+ """
15
+ Implementation of a Simple Agent, which is a simple while loop built right on top of an [`MCPClient`].
16
+
17
+ > [!WARNING]
18
+ > This class is experimental and might be subject to breaking changes in the future without prior notice.
19
+
20
+ Args:
21
+ model (`str`, *optional*):
22
+ The model to run inference with. Can be a model id hosted on the Hugging Face Hub, e.g. `meta-llama/Meta-Llama-3-8B-Instruct`
23
+ or a URL to a deployed Inference Endpoint or other local or remote endpoint.
24
+ servers (`Iterable[dict]`):
25
+ MCP servers to connect to. Each server is a dictionary containing a `type` key and a `config` key. The `type` key can be `"stdio"` or `"sse"`, and the `config` key is a dictionary of arguments for the server.
26
+ provider (`str`, *optional*):
27
+ Name of the provider to use for inference. Defaults to "auto" i.e. the first of the providers available for the model, sorted by the user's order in https://hf.co/settings/inference-providers.
28
+ If model is a URL or `base_url` is passed, then `provider` is not used.
29
+ base_url (`str`, *optional*):
30
+ The base URL to run inference. Defaults to None.
31
+ api_key (`str`, *optional*):
32
+ Token to use for authentication. Will default to the locally Hugging Face saved token if not provided. You can also use your own provider API key to interact directly with the provider's service.
33
+ prompt (`str`, *optional*):
34
+ The system prompt to use for the agent. Defaults to the default system prompt in `constants.py`.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ *,
40
+ model: Optional[str] = None,
41
+ servers: Iterable[ServerConfig],
42
+ provider: Optional[PROVIDER_OR_POLICY_T] = None,
43
+ base_url: Optional[str] = None,
44
+ api_key: Optional[str] = None,
45
+ prompt: Optional[str] = None,
46
+ ):
47
+ super().__init__(model=model, provider=provider, base_url=base_url, api_key=api_key)
48
+ self._servers_cfg = list(servers)
49
+ self.messages: list[Union[dict, ChatCompletionInputMessage]] = [
50
+ {"role": "system", "content": prompt or DEFAULT_SYSTEM_PROMPT}
51
+ ]
52
+
53
+ async def load_tools(self) -> None:
54
+ for cfg in self._servers_cfg:
55
+ await self.add_mcp_server(**cfg)
56
+
57
+ async def run(
58
+ self,
59
+ user_input: str,
60
+ *,
61
+ abort_event: Optional[asyncio.Event] = None,
62
+ ) -> AsyncGenerator[Union[ChatCompletionStreamOutput, ChatCompletionInputMessage], None]:
63
+ """
64
+ Run the agent with the given user input.
65
+
66
+ Args:
67
+ user_input (`str`):
68
+ The user input to run the agent with.
69
+ abort_event (`asyncio.Event`, *optional*):
70
+ An event that can be used to abort the agent. If the event is set, the agent will stop running.
71
+ """
72
+ self.messages.append({"role": "user", "content": user_input})
73
+
74
+ num_turns: int = 0
75
+ next_turn_should_call_tools = True
76
+
77
+ while True:
78
+ if abort_event and abort_event.is_set():
79
+ return
80
+
81
+ async for item in self.process_single_turn_with_tools(
82
+ self.messages,
83
+ exit_loop_tools=EXIT_LOOP_TOOLS,
84
+ exit_if_first_chunk_no_tool=(num_turns > 0 and next_turn_should_call_tools),
85
+ ):
86
+ yield item
87
+
88
+ num_turns += 1
89
+ last = self.messages[-1]
90
+
91
+ if last.get("role") == "tool" and last.get("name") in {t.function.name for t in EXIT_LOOP_TOOLS}:
92
+ return
93
+
94
+ if last.get("role") != "tool" and num_turns > MAX_NUM_TURNS:
95
+ return
96
+
97
+ if last.get("role") != "tool" and next_turn_should_call_tools:
98
+ return
99
+
100
+ next_turn_should_call_tools = last.get("role") != "tool"
@@ -0,0 +1,247 @@
1
+ import asyncio
2
+ import os
3
+ import signal
4
+ import traceback
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich import print
9
+
10
+ from ._cli_hacks import _async_prompt, _patch_anyio_open_process
11
+ from .agent import Agent
12
+ from .utils import _load_agent_config
13
+
14
+
15
+ app = typer.Typer(
16
+ rich_markup_mode="rich",
17
+ help="A squad of lightweight composable AI applications built on Hugging Face's Inference Client and MCP stack.",
18
+ )
19
+
20
+ run_cli = typer.Typer(
21
+ name="run",
22
+ help="Run the Agent in the CLI",
23
+ invoke_without_command=True,
24
+ )
25
+ app.add_typer(run_cli, name="run")
26
+
27
+
28
+ async def run_agent(
29
+ agent_path: Optional[str],
30
+ ) -> None:
31
+ """
32
+ Tiny Agent loop.
33
+
34
+ Args:
35
+ agent_path (`str`, *optional*):
36
+ Path to a local folder containing an `agent.json` and optionally a custom `PROMPT.md` or `AGENTS.md` file or a built-in agent stored in a Hugging Face dataset.
37
+
38
+ """
39
+ _patch_anyio_open_process() # Hacky way to prevent stdio connections to be stopped by Ctrl+C
40
+
41
+ config, prompt = _load_agent_config(agent_path)
42
+
43
+ inputs = config.get("inputs", [])
44
+ servers = config.get("servers", [])
45
+
46
+ abort_event = asyncio.Event()
47
+ exit_event = asyncio.Event()
48
+ first_sigint = True
49
+
50
+ loop = asyncio.get_running_loop()
51
+ original_sigint_handler = signal.getsignal(signal.SIGINT)
52
+
53
+ def _sigint_handler() -> None:
54
+ nonlocal first_sigint
55
+ if first_sigint:
56
+ first_sigint = False
57
+ abort_event.set()
58
+ print("\n[red]Interrupted. Press Ctrl+C again to quit.[/red]", flush=True)
59
+ return
60
+
61
+ print("\n[red]Exiting...[/red]", flush=True)
62
+ exit_event.set()
63
+
64
+ try:
65
+ sigint_registered_in_loop = False
66
+ try:
67
+ loop.add_signal_handler(signal.SIGINT, _sigint_handler)
68
+ sigint_registered_in_loop = True
69
+ except (AttributeError, NotImplementedError):
70
+ # Windows (or any loop that doesn't support it) : fall back to sync
71
+ signal.signal(signal.SIGINT, lambda *_: _sigint_handler())
72
+
73
+ # Handle inputs (i.e. env variables injection)
74
+ resolved_inputs: dict[str, str] = {}
75
+
76
+ if len(inputs) > 0:
77
+ print(
78
+ "[bold blue]Some initial inputs are required by the agent. "
79
+ "Please provide a value or leave empty to load from env.[/bold blue]"
80
+ )
81
+ for input_item in inputs:
82
+ input_id = input_item["id"]
83
+ description = input_item["description"]
84
+ env_special_value = f"${{input:{input_id}}}"
85
+
86
+ # Check if the input is used by any server or as an apiKey
87
+ input_usages = set()
88
+ for server in servers:
89
+ # Check stdio's "env" and http/sse's "headers" mappings
90
+ env_or_headers = server.get("env", {}) if server["type"] == "stdio" else server.get("headers", {})
91
+ for key, value in env_or_headers.items():
92
+ if env_special_value in value:
93
+ input_usages.add(key)
94
+
95
+ raw_api_key = config.get("apiKey")
96
+ if isinstance(raw_api_key, str) and env_special_value in raw_api_key:
97
+ input_usages.add("apiKey")
98
+
99
+ if not input_usages:
100
+ print(
101
+ f"[yellow]Input '{input_id}' defined in config but not used by any server or as an API key."
102
+ " Skipping.[/yellow]"
103
+ )
104
+ continue
105
+
106
+ # Prompt user for input
107
+ env_variable_key = input_id.replace("-", "_").upper()
108
+ print(
109
+ f"[blue] • {input_id}[/blue]: {description}. (default: load from {env_variable_key}).",
110
+ end=" ",
111
+ )
112
+ user_input = (await _async_prompt(exit_event=exit_event)).strip()
113
+ if exit_event.is_set():
114
+ return
115
+
116
+ # Fallback to environment variable when user left blank
117
+ final_value = user_input
118
+ if not final_value:
119
+ final_value = os.getenv(env_variable_key, "")
120
+ if final_value:
121
+ print(f"[green]Value successfully loaded from '{env_variable_key}'[/green]")
122
+ else:
123
+ print(
124
+ f"[yellow]No value found for '{env_variable_key}' in environment variables. Continuing.[/yellow]"
125
+ )
126
+ resolved_inputs[input_id] = final_value
127
+
128
+ # Inject resolved value (can be empty) into stdio's env or http/sse's headers
129
+ for server in servers:
130
+ env_or_headers = server.get("env", {}) if server["type"] == "stdio" else server.get("headers", {})
131
+ for key, value in env_or_headers.items():
132
+ if env_special_value in value:
133
+ env_or_headers[key] = env_or_headers[key].replace(env_special_value, final_value)
134
+
135
+ print()
136
+
137
+ raw_api_key = config.get("apiKey")
138
+ if isinstance(raw_api_key, str):
139
+ substituted_api_key = raw_api_key
140
+ for input_id, val in resolved_inputs.items():
141
+ substituted_api_key = substituted_api_key.replace(f"${{input:{input_id}}}", val)
142
+ config["apiKey"] = substituted_api_key
143
+ # Main agent loop
144
+ async with Agent(
145
+ provider=config.get("provider"), # type: ignore[arg-type]
146
+ model=config.get("model"),
147
+ base_url=config.get("endpointUrl"), # type: ignore[arg-type]
148
+ api_key=config.get("apiKey"),
149
+ servers=servers, # type: ignore[arg-type]
150
+ prompt=prompt,
151
+ ) as agent:
152
+ await agent.load_tools()
153
+ print(f"[bold blue]Agent loaded with {len(agent.available_tools)} tools:[/bold blue]")
154
+ for t in agent.available_tools:
155
+ print(f"[blue] • {t.function.name}[/blue]")
156
+
157
+ while True:
158
+ abort_event.clear()
159
+
160
+ # Check if we should exit
161
+ if exit_event.is_set():
162
+ return
163
+
164
+ try:
165
+ user_input = await _async_prompt(exit_event=exit_event)
166
+ first_sigint = True
167
+ except EOFError:
168
+ print("\n[red]EOF received, exiting.[/red]", flush=True)
169
+ break
170
+ except KeyboardInterrupt:
171
+ if not first_sigint and abort_event.is_set():
172
+ continue
173
+ else:
174
+ print("\n[red]Keyboard interrupt during input processing.[/red]", flush=True)
175
+ break
176
+
177
+ try:
178
+ async for chunk in agent.run(user_input, abort_event=abort_event):
179
+ if abort_event.is_set() and not first_sigint:
180
+ break
181
+ if exit_event.is_set():
182
+ return
183
+
184
+ if hasattr(chunk, "choices"):
185
+ delta = chunk.choices[0].delta
186
+ if delta.content:
187
+ print(delta.content, end="", flush=True)
188
+ if delta.tool_calls:
189
+ for call in delta.tool_calls:
190
+ if call.id:
191
+ print(f"<Tool {call.id}>", end="")
192
+ if call.function.name:
193
+ print(f"{call.function.name}", end=" ")
194
+ if call.function.arguments:
195
+ print(f"{call.function.arguments}", end="")
196
+ else:
197
+ print(
198
+ f"\n\n[green]Tool[{chunk.name}] {chunk.tool_call_id}\n{chunk.content}[/green]\n",
199
+ flush=True,
200
+ )
201
+
202
+ print()
203
+
204
+ except Exception as e:
205
+ tb_str = traceback.format_exc()
206
+ print(f"\n[bold red]Error during agent run: {e}\n{tb_str}[/bold red]", flush=True)
207
+ first_sigint = True # Allow graceful interrupt for the next command
208
+
209
+ except Exception as e:
210
+ tb_str = traceback.format_exc()
211
+ print(f"\n[bold red]An unexpected error occurred: {e}\n{tb_str}[/bold red]", flush=True)
212
+ raise e
213
+
214
+ finally:
215
+ if sigint_registered_in_loop:
216
+ try:
217
+ loop.remove_signal_handler(signal.SIGINT)
218
+ except (AttributeError, NotImplementedError):
219
+ pass
220
+ else:
221
+ signal.signal(signal.SIGINT, original_sigint_handler)
222
+
223
+
224
+ @run_cli.callback()
225
+ def run(
226
+ path: Optional[str] = typer.Argument(
227
+ None,
228
+ help=(
229
+ "Path to a local folder containing an agent.json file or a built-in agent "
230
+ "stored in the 'tiny-agents/tiny-agents' Hugging Face dataset "
231
+ "(https://huggingface.co/datasets/tiny-agents/tiny-agents)"
232
+ ),
233
+ show_default=False,
234
+ ),
235
+ ):
236
+ try:
237
+ asyncio.run(run_agent(path))
238
+ except KeyboardInterrupt:
239
+ print("\n[red]Application terminated by KeyboardInterrupt.[/red]", flush=True)
240
+ raise typer.Exit(code=130)
241
+ except Exception as e:
242
+ print(f"\n[bold red]An unexpected error occurred: {e}[/bold red]", flush=True)
243
+ raise e
244
+
245
+
246
+ if __name__ == "__main__":
247
+ app()