clarifai 11.4.3rc1__py3-none-any.whl → 11.4.5__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 (155) hide show
  1. clarifai/__init__.py +1 -1
  2. clarifai/cli/base.py +1 -2
  3. clarifai/cli/model.py +0 -2
  4. clarifai/client/app.py +2 -1
  5. clarifai/client/auth/helper.py +6 -4
  6. clarifai/client/compute_cluster.py +2 -1
  7. clarifai/client/dataset.py +2 -1
  8. clarifai/client/deployment.py +2 -1
  9. clarifai/client/input.py +2 -1
  10. clarifai/client/model.py +2 -1
  11. clarifai/client/model_client.py +2 -2
  12. clarifai/client/module.py +2 -1
  13. clarifai/client/nodepool.py +2 -1
  14. clarifai/client/runner.py +2 -1
  15. clarifai/client/search.py +2 -1
  16. clarifai/client/user.py +2 -1
  17. clarifai/client/workflow.py +2 -1
  18. clarifai/runners/__init__.py +2 -0
  19. clarifai/runners/models/dummy_openai_model.py +197 -0
  20. clarifai/runners/models/mcp_class.py +19 -39
  21. clarifai/runners/models/model_builder.py +46 -42
  22. clarifai/runners/models/openai_class.py +219 -0
  23. clarifai/runners/utils/code_script.py +41 -15
  24. clarifai/runners/utils/data_types/data_types.py +48 -0
  25. clarifai/runners/utils/data_utils.py +67 -43
  26. clarifai/runners/utils/method_signatures.py +0 -20
  27. clarifai/runners/utils/openai_convertor.py +103 -0
  28. clarifai/urls/helper.py +80 -12
  29. clarifai/utils/config.py +1 -1
  30. clarifai/utils/constants.py +4 -0
  31. {clarifai-11.4.3rc1.dist-info → clarifai-11.4.5.dist-info}/METADATA +13 -2
  32. clarifai-11.4.5.dist-info/RECORD +114 -0
  33. {clarifai-11.4.3rc1.dist-info → clarifai-11.4.5.dist-info}/WHEEL +1 -1
  34. clarifai/__pycache__/__init__.cpython-312.pyc +0 -0
  35. clarifai/__pycache__/__init__.cpython-39.pyc +0 -0
  36. clarifai/__pycache__/errors.cpython-312.pyc +0 -0
  37. clarifai/__pycache__/errors.cpython-39.pyc +0 -0
  38. clarifai/__pycache__/versions.cpython-312.pyc +0 -0
  39. clarifai/__pycache__/versions.cpython-39.pyc +0 -0
  40. clarifai/cli/__pycache__/__init__.cpython-312.pyc +0 -0
  41. clarifai/cli/__pycache__/base.cpython-312.pyc +0 -0
  42. clarifai/cli/__pycache__/compute_cluster.cpython-312.pyc +0 -0
  43. clarifai/cli/__pycache__/deployment.cpython-312.pyc +0 -0
  44. clarifai/cli/__pycache__/model.cpython-312.pyc +0 -0
  45. clarifai/cli/__pycache__/nodepool.cpython-312.pyc +0 -0
  46. clarifai/client/__pycache__/__init__.cpython-312.pyc +0 -0
  47. clarifai/client/__pycache__/__init__.cpython-39.pyc +0 -0
  48. clarifai/client/__pycache__/app.cpython-312.pyc +0 -0
  49. clarifai/client/__pycache__/app.cpython-39.pyc +0 -0
  50. clarifai/client/__pycache__/base.cpython-312.pyc +0 -0
  51. clarifai/client/__pycache__/base.cpython-39.pyc +0 -0
  52. clarifai/client/__pycache__/compute_cluster.cpython-312.pyc +0 -0
  53. clarifai/client/__pycache__/dataset.cpython-312.pyc +0 -0
  54. clarifai/client/__pycache__/deployment.cpython-312.pyc +0 -0
  55. clarifai/client/__pycache__/input.cpython-312.pyc +0 -0
  56. clarifai/client/__pycache__/lister.cpython-312.pyc +0 -0
  57. clarifai/client/__pycache__/model.cpython-312.pyc +0 -0
  58. clarifai/client/__pycache__/model_client.cpython-312.pyc +0 -0
  59. clarifai/client/__pycache__/module.cpython-312.pyc +0 -0
  60. clarifai/client/__pycache__/nodepool.cpython-312.pyc +0 -0
  61. clarifai/client/__pycache__/runner.cpython-312.pyc +0 -0
  62. clarifai/client/__pycache__/search.cpython-312.pyc +0 -0
  63. clarifai/client/__pycache__/user.cpython-312.pyc +0 -0
  64. clarifai/client/__pycache__/workflow.cpython-312.pyc +0 -0
  65. clarifai/client/auth/__pycache__/__init__.cpython-312.pyc +0 -0
  66. clarifai/client/auth/__pycache__/__init__.cpython-39.pyc +0 -0
  67. clarifai/client/auth/__pycache__/helper.cpython-312.pyc +0 -0
  68. clarifai/client/auth/__pycache__/helper.cpython-39.pyc +0 -0
  69. clarifai/client/auth/__pycache__/register.cpython-312.pyc +0 -0
  70. clarifai/client/auth/__pycache__/register.cpython-39.pyc +0 -0
  71. clarifai/client/auth/__pycache__/stub.cpython-312.pyc +0 -0
  72. clarifai/client/auth/__pycache__/stub.cpython-39.pyc +0 -0
  73. clarifai/constants/__pycache__/base.cpython-312.pyc +0 -0
  74. clarifai/constants/__pycache__/base.cpython-39.pyc +0 -0
  75. clarifai/constants/__pycache__/dataset.cpython-312.pyc +0 -0
  76. clarifai/constants/__pycache__/input.cpython-312.pyc +0 -0
  77. clarifai/constants/__pycache__/model.cpython-312.pyc +0 -0
  78. clarifai/constants/__pycache__/rag.cpython-312.pyc +0 -0
  79. clarifai/constants/__pycache__/search.cpython-312.pyc +0 -0
  80. clarifai/constants/__pycache__/workflow.cpython-312.pyc +0 -0
  81. clarifai/datasets/__pycache__/__init__.cpython-312.pyc +0 -0
  82. clarifai/datasets/export/__pycache__/__init__.cpython-312.pyc +0 -0
  83. clarifai/datasets/export/__pycache__/inputs_annotations.cpython-312.pyc +0 -0
  84. clarifai/datasets/upload/__pycache__/__init__.cpython-312.pyc +0 -0
  85. clarifai/datasets/upload/__pycache__/base.cpython-312.pyc +0 -0
  86. clarifai/datasets/upload/__pycache__/features.cpython-312.pyc +0 -0
  87. clarifai/datasets/upload/__pycache__/image.cpython-312.pyc +0 -0
  88. clarifai/datasets/upload/__pycache__/multimodal.cpython-312.pyc +0 -0
  89. clarifai/datasets/upload/__pycache__/text.cpython-312.pyc +0 -0
  90. clarifai/datasets/upload/__pycache__/utils.cpython-312.pyc +0 -0
  91. clarifai/datasets/upload/loaders/__pycache__/__init__.cpython-312.pyc +0 -0
  92. clarifai/datasets/upload/loaders/__pycache__/coco_detection.cpython-312.pyc +0 -0
  93. clarifai/datasets/upload/loaders/__pycache__/imagenet_classification.cpython-312.pyc +0 -0
  94. clarifai/modules/__pycache__/__init__.cpython-312.pyc +0 -0
  95. clarifai/modules/__pycache__/css.cpython-312.pyc +0 -0
  96. clarifai/rag/__pycache__/__init__.cpython-312.pyc +0 -0
  97. clarifai/rag/__pycache__/rag.cpython-312.pyc +0 -0
  98. clarifai/rag/__pycache__/utils.cpython-312.pyc +0 -0
  99. clarifai/runners/__pycache__/__init__.cpython-312.pyc +0 -0
  100. clarifai/runners/__pycache__/__init__.cpython-39.pyc +0 -0
  101. clarifai/runners/__pycache__/server.cpython-312.pyc +0 -0
  102. clarifai/runners/models/__pycache__/__init__.cpython-312.pyc +0 -0
  103. clarifai/runners/models/__pycache__/__init__.cpython-39.pyc +0 -0
  104. clarifai/runners/models/__pycache__/base_typed_model.cpython-312.pyc +0 -0
  105. clarifai/runners/models/__pycache__/mcp_class.cpython-312.pyc +0 -0
  106. clarifai/runners/models/__pycache__/model_builder.cpython-312.pyc +0 -0
  107. clarifai/runners/models/__pycache__/model_builder.cpython-39.pyc +0 -0
  108. clarifai/runners/models/__pycache__/model_class.cpython-312.pyc +0 -0
  109. clarifai/runners/models/__pycache__/model_run_locally.cpython-312.pyc +0 -0
  110. clarifai/runners/models/__pycache__/model_runner.cpython-312.pyc +0 -0
  111. clarifai/runners/models/__pycache__/model_servicer.cpython-312.pyc +0 -0
  112. clarifai/runners/models/__pycache__/test_model_builder.cpython-312-pytest-8.3.5.pyc +0 -0
  113. clarifai/runners/models/base_typed_model.py +0 -238
  114. clarifai/runners/models/example_mcp_server.py +0 -44
  115. clarifai/runners/models/mcp_class.py~ +0 -149
  116. clarifai/runners/models/test_model_builder.py +0 -89
  117. clarifai/runners/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  118. clarifai/runners/utils/__pycache__/code_script.cpython-312.pyc +0 -0
  119. clarifai/runners/utils/__pycache__/const.cpython-312.pyc +0 -0
  120. clarifai/runners/utils/__pycache__/data_handler.cpython-312.pyc +0 -0
  121. clarifai/runners/utils/__pycache__/data_types.cpython-312.pyc +0 -0
  122. clarifai/runners/utils/__pycache__/data_utils.cpython-312.pyc +0 -0
  123. clarifai/runners/utils/__pycache__/loader.cpython-312.pyc +0 -0
  124. clarifai/runners/utils/__pycache__/method_signatures.cpython-312.pyc +0 -0
  125. clarifai/runners/utils/__pycache__/serializers.cpython-312.pyc +0 -0
  126. clarifai/runners/utils/__pycache__/url_fetcher.cpython-312.pyc +0 -0
  127. clarifai/runners/utils/data_handler.py +0 -231
  128. clarifai/runners/utils/data_types/__pycache__/__init__.cpython-312.pyc +0 -0
  129. clarifai/runners/utils/data_types/__pycache__/data_types.cpython-312.pyc +0 -0
  130. clarifai/schema/__pycache__/search.cpython-312.pyc +0 -0
  131. clarifai/urls/__pycache__/helper.cpython-312.pyc +0 -0
  132. clarifai/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  133. clarifai/utils/__pycache__/__init__.cpython-39.pyc +0 -0
  134. clarifai/utils/__pycache__/cli.cpython-312.pyc +0 -0
  135. clarifai/utils/__pycache__/config.cpython-312.pyc +0 -0
  136. clarifai/utils/__pycache__/constants.cpython-312.pyc +0 -0
  137. clarifai/utils/__pycache__/constants.cpython-39.pyc +0 -0
  138. clarifai/utils/__pycache__/logging.cpython-312.pyc +0 -0
  139. clarifai/utils/__pycache__/logging.cpython-39.pyc +0 -0
  140. clarifai/utils/__pycache__/misc.cpython-312.pyc +0 -0
  141. clarifai/utils/__pycache__/misc.cpython-39.pyc +0 -0
  142. clarifai/utils/__pycache__/model_train.cpython-312.pyc +0 -0
  143. clarifai/utils/__pycache__/protobuf.cpython-312.pyc +0 -0
  144. clarifai/utils/config.py~ +0 -145
  145. clarifai/utils/evaluation/__pycache__/__init__.cpython-312.pyc +0 -0
  146. clarifai/utils/evaluation/__pycache__/helpers.cpython-312.pyc +0 -0
  147. clarifai/utils/evaluation/__pycache__/main.cpython-312.pyc +0 -0
  148. clarifai/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
  149. clarifai/workflows/__pycache__/export.cpython-312.pyc +0 -0
  150. clarifai/workflows/__pycache__/utils.cpython-312.pyc +0 -0
  151. clarifai/workflows/__pycache__/validate.cpython-312.pyc +0 -0
  152. clarifai-11.4.3rc1.dist-info/RECORD +0 -230
  153. {clarifai-11.4.3rc1.dist-info → clarifai-11.4.5.dist-info}/entry_points.txt +0 -0
  154. {clarifai-11.4.3rc1.dist-info → clarifai-11.4.5.dist-info/licenses}/LICENSE +0 -0
  155. {clarifai-11.4.3rc1.dist-info → clarifai-11.4.5.dist-info}/top_level.txt +0 -0
@@ -413,6 +413,12 @@ class ModelBuilder:
413
413
  def get_method_signatures(self, mocking=True):
414
414
  """
415
415
  Returns the method signatures for the model class.
416
+
417
+ Args:
418
+ mocking (bool): Whether to mock the model class or not. Defaults to False.
419
+
420
+ Returns:
421
+ list: A list of method signatures for the model class.
416
422
  """
417
423
  model_class = self.load_model_class(mocking=mocking)
418
424
  method_info = model_class._get_method_info()
@@ -436,22 +442,42 @@ class ModelBuilder:
436
442
  return self._client
437
443
 
438
444
  @property
439
- def model_url(self):
445
+ def model_ui_url(self):
446
+ url_helper = ClarifaiUrlHelper(self._client.auth_helper)
447
+ # Note(zeiler): the UI experience isn't the best when including version id right now.
448
+ # if self.model_version_id is not None:
449
+ # return url_helper.clarifai_url(
450
+ # self.client.user_app_id.user_id,
451
+ # self.client.user_app_id.app_id,
452
+ # "models",
453
+ # self.model_id,
454
+ # self.model_version_id,
455
+ # )
456
+ # else:
457
+ return url_helper.clarifai_url(
458
+ self.client.user_app_id.user_id,
459
+ self.client.user_app_id.app_id,
460
+ "models",
461
+ self.model_id,
462
+ )
463
+
464
+ @property
465
+ def model_api_url(self):
440
466
  url_helper = ClarifaiUrlHelper(self._client.auth_helper)
441
467
  if self.model_version_id is not None:
442
- return url_helper.clarifai_url(
468
+ return url_helper.api_url(
443
469
  self.client.user_app_id.user_id,
444
470
  self.client.user_app_id.app_id,
445
471
  "models",
446
472
  self.model_id,
473
+ self.model_version_id,
447
474
  )
448
475
  else:
449
- return url_helper.clarifai_url(
476
+ return url_helper.api_url(
450
477
  self.client.user_app_id.user_id,
451
478
  self.client.user_app_id.app_id,
452
479
  "models",
453
480
  self.model_id,
454
- self.model_version_id,
455
481
  )
456
482
 
457
483
  def _get_model_proto(self):
@@ -930,44 +956,20 @@ class ModelBuilder:
930
956
  return
931
957
  self.model_version_id = response.model_version_id
932
958
  logger.info(f"Created Model Version ID: {self.model_version_id}")
933
- logger.info(f"Full url to that version is: {self.model_url}")
959
+ logger.info(f"Full url to that version is: {self.model_ui_url}")
934
960
  try:
935
961
  is_uploaded = self.monitor_model_build()
936
962
  if is_uploaded:
937
- # Provide an mcp client config
938
- if model_type_id == "mcp":
939
- snippet = (
940
- """
941
- import asyncio
942
- import os
943
- from fastmcp import Client
944
- from fastmcp.client.transports import StreamableHttpTransport
945
-
946
- transport = StreamableHttpTransport(url="%s/mcp",
947
- headers={"Authorization": "Bearer " + os.environ["CLARIFAI_PAT"]})
948
-
949
- async def main():
950
- async with Client(transport) as client:
951
- tools = await client.list_tools()
952
- print(f"Available tools: {tools}")
953
- result = await client.call_tool(tools[0].name, {"a": 5, "b": 3})
954
- print(f"Result: {result[0].text}")
955
-
956
- if __name__ == "__main__":
957
- asyncio.run(main())
958
- """
959
- % self.model_url
960
- )
961
- else: # python code to run the model.
962
- from clarifai.runners.utils import code_script
963
-
964
- method_signatures = self.get_method_signatures()
965
- snippet = code_script.generate_client_script(
966
- method_signatures,
967
- user_id=self.client.user_app_id.user_id,
968
- app_id=self.client.user_app_id.app_id,
969
- model_id=self.model_proto.id,
970
- )
963
+ # python code to run the model.
964
+ from clarifai.runners.utils import code_script
965
+
966
+ method_signatures = self.get_method_signatures()
967
+ snippet = code_script.generate_client_script(
968
+ method_signatures,
969
+ user_id=self.client.user_app_id.user_id,
970
+ app_id=self.client.user_app_id.app_id,
971
+ model_id=self.model_proto.id,
972
+ )
971
973
  logger.info("""\n
972
974
  XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
973
975
  # Here is a code snippet to use this model:
@@ -1074,7 +1076,7 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1074
1076
  logger.info("Model build complete!")
1075
1077
  logger.info(f"Build time elapsed {time.time() - st:.1f}s)")
1076
1078
  logger.info(
1077
- f"Check out the model at {self.model_url} version: {self.model_version_id}"
1079
+ f"Check out the model at {self.model_ui_url} version: {self.model_version_id}"
1078
1080
  )
1079
1081
  return True
1080
1082
  else:
@@ -1099,10 +1101,12 @@ def upload_model(folder, stage, skip_dockerfile):
1099
1101
  exists = builder.check_model_exists()
1100
1102
  if exists:
1101
1103
  logger.info(
1102
- f"Model already exists at {builder.model_url}, this upload will create a new version for it."
1104
+ f"Model already exists at {builder.model_ui_url}, this upload will create a new version for it."
1103
1105
  )
1104
1106
  else:
1105
- logger.info(f"New model will be created at {builder.model_url} with it's first version.")
1107
+ logger.info(
1108
+ f"New model will be created at {builder.model_ui_url} with it's first version."
1109
+ )
1106
1110
 
1107
1111
  input("Press Enter to continue...")
1108
1112
  builder.upload_model_version()
@@ -0,0 +1,219 @@
1
+ """Base class for creating OpenAI-compatible API server."""
2
+
3
+ import json
4
+ from typing import Any, Dict, Iterator
5
+
6
+ from clarifai.runners.models.model_class import ModelClass
7
+
8
+
9
+ class OpenAIModelClass(ModelClass):
10
+ """Base class for wrapping OpenAI-compatible servers as a model running in Clarifai.
11
+ This handles all the transport between the API and the OpenAI-compatible server.
12
+
13
+ To use this class, create a subclass and set the following class attributes:
14
+ - openai_client: The OpenAI-compatible client instance
15
+ - model_name: The name of the model to use with the client
16
+
17
+ Example:
18
+ class MyOpenAIModel(OpenAIModelClass):
19
+ openai_client = OpenAI(api_key="your-key")
20
+ model_name = "gpt-4"
21
+ """
22
+
23
+ # These should be overridden in subclasses
24
+ openai_client = None
25
+ model_name = None
26
+
27
+ def __init__(self) -> None:
28
+ if self.openai_client is None:
29
+ raise NotImplementedError("Subclasses must set the 'openai_client' class attribute")
30
+ if self.model_name is None:
31
+ self.model_name = self.openai_client.models.list().data[0].id
32
+
33
+ self.client = self.openai_client
34
+ self.model = self.model_name
35
+
36
+ def _extract_request_params(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
37
+ """Extract and validate common openai arguments parameters from the request data.
38
+
39
+ Args:
40
+ request_data: The parsed JSON request data
41
+
42
+ Returns:
43
+ Dict containing the extracted parameters
44
+ """
45
+ return {
46
+ "messages": request_data.get("messages", []),
47
+ "temperature": request_data.get("temperature", 1.0),
48
+ "max_tokens": request_data.get("max_tokens"),
49
+ "max_completion_tokens": request_data.get("max_completion_tokens"),
50
+ "n": request_data.get("n", 1),
51
+ "frequency_penalty": request_data.get("frequency_penalty"),
52
+ "presence_penalty": request_data.get("presence_penalty"),
53
+ "top_p": request_data.get("top_p", 1.0),
54
+ "reasoning_effort": request_data.get("reasoning_effort"),
55
+ "response_format": request_data.get("response_format"),
56
+ "stop": request_data.get("stop"),
57
+ "tools": request_data.get("tools"),
58
+ "tool_choice": request_data.get("tool_choice"),
59
+ "tool_resources": request_data.get("tool_resources"),
60
+ "modalities": request_data.get("modalities"),
61
+ "stream_options": request_data.get("stream_options", {"include_usage": True}),
62
+ }
63
+
64
+ def _create_completion_args(
65
+ self, params: Dict[str, Any], stream: bool = False
66
+ ) -> Dict[str, Any]:
67
+ """Create the completion arguments dictionary from parameters.
68
+
69
+ Args:
70
+ params: Dictionary of parameters extracted from request
71
+ stream: Whether this is a streaming request
72
+
73
+ Returns:
74
+ Dict containing the completion arguments
75
+ """
76
+ completion_args = {
77
+ "model": self.model,
78
+ "messages": params["messages"],
79
+ "temperature": params["temperature"],
80
+ }
81
+
82
+ if stream:
83
+ completion_args["stream"] = True
84
+ if params.get("stream_options"):
85
+ completion_args["stream_options"] = params["stream_options"]
86
+
87
+ # Add optional parameters if they exist
88
+ optional_params = [
89
+ "max_tokens",
90
+ "max_completion_tokens",
91
+ "n",
92
+ "frequency_penalty",
93
+ "presence_penalty",
94
+ "top_p",
95
+ "reasoning_effort",
96
+ "response_format",
97
+ "stop",
98
+ "tools",
99
+ "tool_choice",
100
+ "tool_resources",
101
+ "modalities",
102
+ ]
103
+
104
+ for param in optional_params:
105
+ if params.get(param) is not None:
106
+ completion_args[param] = params[param]
107
+
108
+ return completion_args
109
+
110
+ def _format_error_response(self, error: Exception) -> str:
111
+ """Format an error response in OpenAI-compatible format.
112
+
113
+ Args:
114
+ error: The exception that occurred
115
+
116
+ Returns:
117
+ JSON string containing the error response
118
+ """
119
+ error_response = {
120
+ "error": {
121
+ "message": str(error),
122
+ "type": "InvalidRequestError",
123
+ "code": "invalid_request_error",
124
+ }
125
+ }
126
+ return json.dumps(error_response)
127
+
128
+ @ModelClass.method
129
+ def openai_transport(self, msg: str) -> str:
130
+ """The single model method to get the OpenAI-compatible request and send it to the OpenAI server
131
+ then return its response.
132
+
133
+ Args:
134
+ msg: JSON string containing the request parameters
135
+
136
+ Returns:
137
+ JSON string containing the response or error
138
+ """
139
+ try:
140
+ request_data = json.loads(msg)
141
+ params = self._extract_request_params(request_data)
142
+ stream = request_data.get("stream", False)
143
+
144
+ if stream:
145
+ chunks = self._process_streaming_request(**params)
146
+ response_list = []
147
+ for chunk in chunks:
148
+ response_list.append(chunk)
149
+ return json.dumps(response_list)
150
+ else:
151
+ completion = self._process_request(**params)
152
+ if completion.get('usage'):
153
+ if completion['usage'].get('prompt_tokens') and completion['usage'].get(
154
+ 'completion_tokens'
155
+ ):
156
+ self.set_output_context(
157
+ prompt_tokens=completion['usage']['prompt_tokens'],
158
+ completion_tokens=completion['usage']['completion_tokens'],
159
+ )
160
+
161
+ return json.dumps(completion)
162
+
163
+ except Exception as e:
164
+ return self._format_error_response(e)
165
+
166
+ @ModelClass.method
167
+ def openai_stream_transport(self, msg: str) -> Iterator[str]:
168
+ """Process an OpenAI-compatible request and return a streaming response iterator.
169
+ This method is used when stream=True and returns an iterator of strings directly,
170
+ without converting to a list or JSON serializing.
171
+
172
+ Args:
173
+ msg: The request as a JSON string.
174
+
175
+ Returns:
176
+ Iterator[str]: An iterator yielding text chunks from the streaming response.
177
+ """
178
+ try:
179
+ request_data = json.loads(msg)
180
+ params = self._extract_request_params(request_data)
181
+ for chunk in self._process_streaming_request(**params):
182
+ if chunk.get('usage'):
183
+ if chunk['usage'].get('prompt_tokens') and chunk['usage'].get(
184
+ 'completion_tokens'
185
+ ):
186
+ self.set_output_context(
187
+ prompt_tokens=chunk['usage']['prompt_tokens'],
188
+ completion_tokens=chunk['usage']['completion_tokens'],
189
+ )
190
+ yield json.dumps(chunk)
191
+ except Exception as e:
192
+ yield f"Error: {str(e)}"
193
+
194
+ def _process_request(self, **kwargs) -> Any:
195
+ """Process a standard (non-streaming) request using the OpenAI client.
196
+
197
+ Args:
198
+ **kwargs: Request parameters
199
+
200
+ Returns:
201
+ The completion response from the OpenAI client
202
+ """
203
+ completion_args = self._create_completion_args(kwargs)
204
+ return self.client.chat.completions.create(**completion_args).to_dict()
205
+
206
+ def _process_streaming_request(self, **kwargs) -> Iterator[str]:
207
+ """Process a streaming request using the OpenAI client.
208
+
209
+ Args:
210
+ **kwargs: Request parameters
211
+
212
+ Returns:
213
+ Iterator yielding response chunks
214
+ """
215
+ completion_args = self._create_completion_args(kwargs, stream=True)
216
+ completion_stream = self.client.chat.completions.create(**completion_args)
217
+
218
+ for chunk in completion_stream:
219
+ yield chunk.to_dict()
@@ -4,6 +4,7 @@ from typing import List
4
4
  from clarifai_grpc.grpc.api import resources_pb2
5
5
 
6
6
  from clarifai.runners.utils import data_utils
7
+ from clarifai.urls.helper import ClarifaiUrlHelper
7
8
 
8
9
 
9
10
  def generate_client_script(
@@ -15,6 +16,38 @@ def generate_client_script(
15
16
  deployment_id: str = None,
16
17
  use_ctx: bool = False,
17
18
  ) -> str:
19
+ url_helper = ClarifaiUrlHelper()
20
+
21
+ # Provide an mcp client config
22
+ if len(method_signatures) == 1 and method_signatures[0].name == "mcp_transport":
23
+ api_url = url_helper.api_url(
24
+ user_id,
25
+ app_id,
26
+ "models",
27
+ model_id,
28
+ )
29
+
30
+ _CLIENT_TEMPLATE = """
31
+ import asyncio
32
+ import os
33
+ from fastmcp import Client
34
+ from fastmcp.client.transports import StreamableHttpTransport
35
+
36
+ transport = StreamableHttpTransport(url="%s/mcp",
37
+ headers={"Authorization": "Bearer " + os.environ["CLARIFAI_PAT"]})
38
+
39
+ async def main():
40
+ async with Client(transport) as client:
41
+ tools = await client.list_tools()
42
+ print(f"Available tools: {tools}")
43
+ result = await client.call_tool(tools[0].name, {"a": 5, "b": 3})
44
+ print(f"Result: {result[0].text}")
45
+
46
+ if __name__ == "__main__":
47
+ asyncio.run(main())
48
+ """
49
+ return _CLIENT_TEMPLATE % api_url
50
+
18
51
  _CLIENT_TEMPLATE = """\
19
52
  import os
20
53
 
@@ -35,8 +68,9 @@ from clarifai.runners.utils import data_types
35
68
  model_section = """
36
69
  model = Model.from_current_context()"""
37
70
  else:
71
+ model_ui_url = url_helper.clarifai_url(user_id, app_id, "models", model_id)
38
72
  model_section = f"""
39
- model = Model("https://clarifai.com/{user_id}/{app_id}/models/{model_id}",
73
+ model = Model({model_ui_url},
40
74
  deployment_id = {deployment_id}, # Only needed for dedicated deployed models
41
75
  {base_url_str}
42
76
  )
@@ -53,13 +87,10 @@ model = Model("https://clarifai.com/{user_id}/{app_id}/models/{model_id}",
53
87
  method_name = method_signature.name
54
88
  client_script_str = f'response = model.{method_name}('
55
89
  annotations = _get_annotations_source(method_signature)
56
- for param_name, (param_type, default_value) in annotations.items():
57
- print(
58
- f"param_name: {param_name}, param_type: {param_type}, default_value: {default_value}"
59
- )
90
+ for param_name, (param_type, default_value, required) in annotations.items():
60
91
  if param_name == "return":
61
92
  continue
62
- if default_value is None:
93
+ if default_value is None and required:
63
94
  default_value = _set_default_value(param_type)
64
95
  client_script_str += f"{param_name}={default_value}, "
65
96
  client_script_str = client_script_str.rstrip(", ") + ")"
@@ -98,7 +129,7 @@ def _get_annotations_source(method_signature: resources_pb2.MethodSignature) ->
98
129
  default_value = None
99
130
  if data_utils.Param.get_default(input_field):
100
131
  default_value = _parse_default_value(input_field)
101
- annotations[param_name] = (param_type, default_value)
132
+ annotations[param_name] = (param_type, default_value, input_field.required)
102
133
  if not method_signature.output_fields:
103
134
  raise ValueError("MethodSignature must have at least one output field")
104
135
  for output_field in method_signature.output_fields:
@@ -106,7 +137,7 @@ def _get_annotations_source(method_signature: resources_pb2.MethodSignature) ->
106
137
  param_type = _get_base_type(output_field)
107
138
  if output_field.iterator:
108
139
  param_type = f"Iterator[{param_type}]"
109
- annotations[param_name] = (param_type, None)
140
+ annotations[param_name] = (param_type, None, output_field.required)
110
141
  return annotations
111
142
 
112
143
 
@@ -155,7 +186,7 @@ def _map_default_value(field_type):
155
186
  default_value = None
156
187
 
157
188
  if field_type == "str":
158
- default_value = 'What is the future of AI?'
189
+ default_value = repr('What is the future of AI?')
159
190
  elif field_type == "bytes":
160
191
  default_value = b""
161
192
  elif field_type == "int":
@@ -190,11 +221,9 @@ def _set_default_value(field_type):
190
221
  Set the default value of a field if it is not set.
191
222
  """
192
223
  is_iterator = False
193
- print(f"before field_type: {field_type}")
194
224
  if field_type.startswith("Iterator["):
195
225
  is_iterator = True
196
226
  field_type = field_type[9:-1]
197
- print(f"after field_type: {field_type}")
198
227
  default_value = None
199
228
  default_value = _map_default_value(field_type)
200
229
  if field_type.startswith("List["):
@@ -211,11 +240,8 @@ def _set_default_value(field_type):
211
240
  element_type_defaults = [_map_default_value(et) for et in element_types]
212
241
  default_value = f"{{{', '.join([str(et) for et in element_type_defaults])}}}"
213
242
 
214
- if field_type == 'str':
215
- default_value = repr(default_value)
216
243
  if is_iterator:
217
244
  default_value = f'iter([{default_value}])'
218
- print(f"after default_value: {default_value}")
219
245
  return default_value
220
246
 
221
247
 
@@ -233,7 +259,7 @@ def _parse_default_value(field: resources_pb2.ModelTypeField):
233
259
  elif data_type == resources_pb2.ModelTypeField.DataType.BOOL:
234
260
  return 'True' if default_str.lower() == 'true' else 'False'
235
261
  elif data_type == resources_pb2.ModelTypeField.DataType.STR:
236
- return repr(default_str)
262
+ return default_str
237
263
  elif data_type == resources_pb2.ModelTypeField.DataType.BYTES:
238
264
  return f"b{repr(default_str.encode('utf-8'))}"
239
265
  elif data_type == resources_pb2.ModelTypeField.DataType.JSON_DATA:
@@ -395,6 +395,22 @@ class Image(MessageData):
395
395
  raise ValueError("Image has no bytes")
396
396
  return PILImage.open(io.BytesIO(self.proto.base64))
397
397
 
398
+ def to_base64_str(self) -> str:
399
+ if not self.proto.base64:
400
+ raise ValueError("Image has no bytes")
401
+ if isinstance(self.proto.base64, str):
402
+ return self.proto.base64
403
+ if isinstance(self.proto.base64, bytes):
404
+ try:
405
+ # trying direct decode (if already a base64 bytes)
406
+ return self.proto.base64.decode('utf-8')
407
+ except UnicodeDecodeError:
408
+ import base64
409
+
410
+ return base64.b64encode(self.proto.base64).decode('utf-8')
411
+ else:
412
+ raise TypeError("Expected str or bytes for Image.base64")
413
+
398
414
  def to_numpy(self) -> np.ndarray:
399
415
  return np.asarray(self.to_pil())
400
416
 
@@ -466,6 +482,22 @@ class Audio(MessageData):
466
482
  def to_proto(self) -> AudioProto:
467
483
  return self.proto
468
484
 
485
+ def to_base64_str(self) -> str:
486
+ if not self.proto.base64:
487
+ raise ValueError("Audio has no bytes")
488
+ if isinstance(self.proto.base64, str):
489
+ return self.proto.base64
490
+ if isinstance(self.proto.base64, bytes):
491
+ try:
492
+ # trying direct decode (if already a base64 bytes)
493
+ return self.proto.base64.decode('utf-8')
494
+ except UnicodeDecodeError:
495
+ import base64
496
+
497
+ return base64.b64encode(self.proto.base64).decode('utf-8')
498
+ else:
499
+ raise TypeError("Expected str or bytes for Audio.base64")
500
+
469
501
  @classmethod
470
502
  def from_proto(cls, proto: AudioProto) -> "Audio":
471
503
  return cls(proto)
@@ -578,6 +610,22 @@ class Video(MessageData):
578
610
  def to_proto(self) -> VideoProto:
579
611
  return self.proto
580
612
 
613
+ def to_base64_str(self) -> str:
614
+ if not self.proto.base64:
615
+ raise ValueError("Video has no bytes")
616
+ if isinstance(self.proto.base64, str):
617
+ return self.proto.base64
618
+ if isinstance(self.proto.base64, bytes):
619
+ try:
620
+ # trying direct decode (if already a base64 bytes)
621
+ return self.proto.base64.decode('utf-8')
622
+ except UnicodeDecodeError:
623
+ import base64
624
+
625
+ return base64.b64encode(self.proto.base64).decode('utf-8')
626
+ else:
627
+ raise TypeError("Expected str or bytes for Video.base64")
628
+
581
629
  @classmethod
582
630
  def from_proto(cls, proto: VideoProto) -> "Video":
583
631
  return cls(proto)