diff --git a/clarifai/cli/model.py b/clarifai/cli/model.py index 25140c47..caa80a5f 100644 --- a/clarifai/cli/model.py +++ b/clarifai/cli/model.py @@ -505,7 +505,7 @@ def local_dev(ctx, model_path): logger.info(f"Current deployment_id: {deployment_id}") logger.info( - f"Full url for the model: /users/{user_id}/apps/{app_id}/models/{model.id}/versions/{version.id}" + f"Full url for the model: {ctx.obj.current.ui}/users/{user_id}/apps/{app_id}/models/{model.id}/versions/{version.id}" ) # Now that we have all the context in ctx.obj, we need to update the config.yaml in diff --git a/clarifai/client/model.py b/clarifai/client/model.py index f22f444b..877196e5 100644 --- a/clarifai/client/model.py +++ b/clarifai/client/model.py @@ -123,6 +123,7 @@ def __init__( @classmethod def from_current_context(cls, **kwargs) -> 'Model': + from clarifai.urls.helper import ClarifaiUrlHelper, auth_obj from clarifai.utils.config import Config current = Config.from_yaml().current @@ -130,7 +131,8 @@ def from_current_context(cls, **kwargs) -> 'Model': # set the current context to env vars. current.set_to_env() - url = f"https://clarifai.com/{current.user_id}/{current.app_id}/models/{current.model_id}" + url_helper = ClarifaiUrlHelper(auth_obj(ui=current.ui, base=current.api_base)) + url = url_helper.clarifai_url(current.user_id, current.app_id, "models", current.model_id) # construct the Model object. kwargs = {} diff --git a/clarifai/runners/utils/code_script.py b/clarifai/runners/utils/code_script.py index e56f76f8..f28e7c1f 100644 --- a/clarifai/runners/utils/code_script.py +++ b/clarifai/runners/utils/code_script.py @@ -4,7 +4,22 @@ from clarifai_grpc.grpc.api import resources_pb2 from clarifai.runners.utils import data_utils -from clarifai.urls.helper import ClarifaiUrlHelper +from clarifai.urls.helper import ClarifaiUrlHelper, auth_obj +from clarifai.utils.config import Config +from clarifai.utils.constants import MCP_TRANSPORT_NAME, OPENAI_TRANSPORT_NAME + + +def has_signature_method( + name: str, method_signatures: List[resources_pb2.MethodSignature] +) -> bool: + """ + Check if a method signature with the given name exists in the list of method signatures. + + :param name: The name of the method to check. + :param method_signatures: List of MethodSignature objects to search in. + :return: True if a method with the given name exists, False otherwise. + """ + return any(method_signature.name == name for method_signature in method_signatures) def generate_client_script( @@ -16,14 +31,18 @@ def generate_client_script( deployment_id: str = None, use_ctx: bool = False, ) -> str: - url_helper = ClarifaiUrlHelper() + if use_ctx: + # use the config values. + current = Config.from_yaml().current + url_helper = ClarifaiUrlHelper(auth_obj(ui=current.ui, base=current.api_base)) + else: + url_helper = ClarifaiUrlHelper() - # Provide an mcp client config - if len(method_signatures) == 1 and method_signatures[0].name == "mcp_transport": - api_url = url_helper.api_url( + # Provide an mcp client config if there is a method named "mcp_transport" + if has_signature_method(MCP_TRANSPORT_NAME, method_signatures): + mcp_url = url_helper.mcp_api_url( user_id, app_id, - "models", model_id, ) @@ -33,20 +52,48 @@ def generate_client_script( from fastmcp import Client from fastmcp.client.transports import StreamableHttpTransport -transport = StreamableHttpTransport(url="%s/mcp", +transport = StreamableHttpTransport(url="%s", headers={"Authorization": "Bearer " + os.environ["CLARIFAI_PAT"]}) async def main(): async with Client(transport) as client: tools = await client.list_tools() print(f"Available tools: {tools}") + # TODO: update the dictionary of arguments passed to call_tool to make sense for your MCP. result = await client.call_tool(tools[0].name, {"a": 5, "b": 3}) print(f"Result: {result[0].text}") if __name__ == "__main__": asyncio.run(main()) """ - return _CLIENT_TEMPLATE % api_url + return _CLIENT_TEMPLATE % mcp_url + + if has_signature_method(OPENAI_TRANSPORT_NAME, method_signatures): + openai_api_base = url_helper.openai_api_url() + model_ui_url = url_helper.clarifai_url(user_id, app_id, "models", model_id) + _CLIENT_TEMPLATE = """ +import os +from openai import OpenAI + +client = OpenAI( + base_url="%s", + api_key=os.environ['CLARIFAI_PAT'], +) +response = client.chat.completions.create( + model="%s", + messages=[ + {"role": "developer", "content": "Talk like a pirate."}, + { + "role": "user", + "content": "How do I check if a Python object is an instance of a class?", + }, + ], + temperature=0.7, + stream=False, # stream=True also works, just iterator over the response +) +print(response) +""" + return _CLIENT_TEMPLATE % (openai_api_base, model_ui_url) _CLIENT_TEMPLATE = """\ import os diff --git a/clarifai/urls/helper.py b/clarifai/urls/helper.py index 7ebd0b3b..0dde1370 100644 --- a/clarifai/urls/helper.py +++ b/clarifai/urls/helper.py @@ -64,6 +64,47 @@ def imv_ui_url(self, dest_user_id, dest_app_id, imv_id): imv_id, ) + def mcp_api_url(self, user_id, app_id, model_id, version_id: str = None): + """We have a special endpoint for MCP hosted models. + + Example: + https://api.clarifai.com/v2/ext/mcp/v1/users/{user_id}/apps/{app_id}/models/{model_id}/versions/{version_id} + + Args: + user_id: the author of the resource. + app_id: the author's app the resource was created in. + model_id: the resource ID + version_id: the version of the resource. + """ + if version_id is None: + return "%s/v2/ext/mcp/v1/users/%s/apps/%s/models/%s" % ( + self.base, + user_id, + app_id, + model_id, + ) + return "%s/v2/ext/mcp/v1/users/%s/apps/%s/models/%s/versions/%s" % ( + self.base, + user_id, + app_id, + model_id, + version_id, + ) + + def openai_api_url(self): + """We have a special endpoint for openAI compatible models. + + This doesn't include the /chat/completions suffix which the openAI client automatically + adds. + + It also doesn't incldue the model which you an set as the model arg in an openAI client call + using the clarifai_url() method below. + + Example: + https://api.clarifai.com/v2/ext/openai/v1 + """ + return "%s/v2/ext/openai/v1" % self.base + def api_url(self, user_id, app_id, resource_type, resource_id, version_id: str = None): """This is the path to the resource in the API. diff --git a/clarifai/utils/config.py b/clarifai/utils/config.py index 2f9dcb72..8ebb8d33 100644 --- a/clarifai/utils/config.py +++ b/clarifai/utils/config.py @@ -4,7 +4,7 @@ import yaml -from clarifai.utils.constants import DEFAULT_CONFIG +from clarifai.utils.constants import DEFAULT_BASE, DEFAULT_CONFIG, DEFAULT_UI class Context(OrderedDict): @@ -23,6 +23,12 @@ def __init__(self, name, **kwargs): self['env'] = kwargs def __getattr__(self, key): + """Get the key from the config. You can pass a lowercase key like "pat" and it will check if + the environment variable CLARIFAI_PAT set and use that first. If no env var, then it checks + if that env var name is in the config and use that. If not then checks if + "pat" is in the config, if not then it falls back to CLARIFAI_PAT in the environment + variables, else raises an AttributeError. + """ try: if key == 'name': return self[key] @@ -32,7 +38,9 @@ def __getattr__(self, key): # Allow accessing CLARIFAI_PAT type env var names from config as .pat envvar_name = 'CLARIFAI_' + key.upper() env = self['env'] - if envvar_name in env: + if envvar_name in os.environ: # environment variable take precedence. + value = os.environ[envvar_name] + elif envvar_name in env: value = env[envvar_name] if value == "ENVVAR": if envvar_name not in os.environ: @@ -40,8 +48,17 @@ def __getattr__(self, key): f"Environment variable '{envvar_name}' not set. Attempting to load it for config '{self['name']}'. Please set it in your terminal." ) return os.environ[envvar_name] - else: + elif key in env: # check if key is in the config value = env[key] + # below are some default fallback values for UI and API base. + elif envvar_name == 'CLARIFAI_UI': + value = DEFAULT_UI + elif envvar_name == 'CLARIFAI_API_BASE': + value = DEFAULT_BASE + else: + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{key}' or '{envvar_name}' and '{envvar_name}' is also not in os.environ:" + ) if isinstance(value, dict): return Context(value) diff --git a/clarifai/utils/constants.py b/clarifai/utils/constants.py index dfb50b1d..8b367ace 100644 --- a/clarifai/utils/constants.py +++ b/clarifai/utils/constants.py @@ -3,6 +3,8 @@ DEFAULT_UI = os.environ.get("CLARIFAI_UI", "https://clarifai.com") DEFAULT_BASE = os.environ.get("CLARIFAI_API_BASE", "https://api.clarifai.com") +MCP_TRANSPORT_NAME = "mcp_transport" +OPENAI_TRANSPORT_NAME = "openai_transport" CLARIFAI_PAT_ENV_VAR = "CLARIFAI_PAT" CLARIFAI_SESSION_TOKEN_ENV_VAR = "CLARIFAI_SESSION_TOKEN"