-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Adding Microsoft Fabric to the list of supported providers and clients #9052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c98909c
a9f9a18
2d590b5
f0e0004
41e2f5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,305 @@ | ||
| """Microsoft Fabric Azure OpenAI integration for DSPy. | ||
| This module provides a custom LM class for using Azure OpenAI models within | ||
| Microsoft Fabric notebooks. It handles authentication and endpoint configuration | ||
| automatically using Fabric's built-in service discovery and token utilities. | ||
| Note: This class only works within a Microsoft Fabric environment. | ||
| """ | ||
|
|
||
| from typing import Any, ClassVar | ||
|
|
||
| import requests | ||
|
|
||
| from dspy.clients.base_lm import BaseLM | ||
|
|
||
|
|
||
| class FabricAzureOpenAI(BaseLM): | ||
| """Language model client for Azure OpenAI in Microsoft Fabric. | ||
| This class provides integration with Azure OpenAI models deployed in Microsoft Fabric. | ||
| It automatically handles authentication and endpoint configuration using Fabric's | ||
| service discovery and token utilities. | ||
| Note: | ||
| This class requires the following packages available in Microsoft Fabric: | ||
| - synapse.ml.fabric.service_discovery | ||
| - synapse.ml.fabric.token_utils | ||
| Supported models: | ||
| - gpt-5 (reasoning model) | ||
| - gpt-4.1 | ||
| - gpt-4.1-mini | ||
| Args: | ||
| deployment_name: The name of the Azure OpenAI deployment to use. | ||
| Must be one of: gpt-5, gpt-4.1, gpt-4.1-mini | ||
| model_type: The type of the model. Defaults to "chat". | ||
| temperature: The sampling temperature. Defaults to 0.0. | ||
| max_tokens: Maximum number of tokens to generate. Defaults to 4000. | ||
| cache: Whether to cache responses. Defaults to True. | ||
| **kwargs: Additional arguments passed to the base class. | ||
| Example: | ||
| ```python | ||
| import dspy | ||
| from dspy.clients import FabricAzureOpenAI | ||
| # In a Microsoft Fabric notebook | ||
| lm = FabricAzureOpenAI(deployment_name="gpt-5") | ||
| dspy.configure(lm=lm) | ||
| # Use with DSPy modules | ||
| predictor = dspy.Predict("question -> answer") | ||
| result = predictor(question="What is DSPy?") | ||
| ``` | ||
| """ | ||
|
|
||
| # Supported models in Microsoft Fabric | ||
| SUPPORTED_MODELS: ClassVar[set[str]] = {"gpt-5", "gpt-4.1", "gpt-4.1-mini"} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why hardcode a static list of supported models? It's already outdated after the release of GPT 5.1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why hardcode a static list of supported models? It's already outdated after the release of GPT 5.1 |
||
| REASONING_MODELS: ClassVar[set[str]] = {"gpt-5"} | ||
|
|
||
| def __init__( | ||
| self, | ||
| deployment_name: str = "gpt-5", | ||
| model_type: str = "chat", | ||
| temperature: float = 0.0, | ||
| max_tokens: int = 4000, | ||
| cache: bool = True, | ||
| **kwargs, | ||
| ): | ||
| """Initialize the FabricAzureOpenAI client. | ||
| Args: | ||
| deployment_name: The Azure OpenAI deployment name. | ||
| model_type: The type of model ("chat" or "text"). | ||
| temperature: Sampling temperature (0.0 to 1.0). | ||
| max_tokens: Maximum tokens to generate. | ||
| cache: Whether to enable caching. | ||
| **kwargs: Additional keyword arguments. | ||
| Raises: | ||
| ValueError: If deployment_name is not a supported model. | ||
| """ | ||
| # Validate model support | ||
| if deployment_name not in self.SUPPORTED_MODELS: | ||
| raise ValueError( | ||
| f"Model '{deployment_name}' is not supported in Microsoft Fabric. " | ||
| f"Supported models are: {', '.join(sorted(self.SUPPORTED_MODELS))}. " | ||
| f"For more information, see: " | ||
| f"https://learn.microsoft.com/en-us/fabric/data-science/ai-services/ai-services-overview" | ||
| ) | ||
|
|
||
| self.deployment_name = deployment_name | ||
| super().__init__( | ||
| model=deployment_name, | ||
| model_type=model_type, | ||
| temperature=temperature, | ||
| max_tokens=max_tokens, | ||
| cache=cache, | ||
| **kwargs, | ||
| ) | ||
|
|
||
| # Check if this is a reasoning model | ||
| self.is_reasoning_model = deployment_name in self.REASONING_MODELS | ||
|
|
||
| def _get_fabric_config(self): | ||
| """Get Fabric environment configuration and auth header. | ||
| Returns: | ||
| tuple: (fabric_env_config, auth_header) | ||
| Raises: | ||
| ImportError: If Fabric SDK packages are not available. | ||
| """ | ||
| try: | ||
| from synapse.ml.fabric.service_discovery import get_fabric_env_config | ||
| from synapse.ml.fabric.token_utils import TokenUtils | ||
| except ImportError as e: | ||
| raise ImportError( | ||
| "Microsoft Fabric SDK packages are required to use FabricAzureOpenAI. " | ||
| "These packages are only available in Microsoft Fabric notebooks. " | ||
| "Please ensure you are running in a Fabric environment." | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes it virtually impossible for any project to accept and maintain this LM provider. Not only would you need a Microsoft account tot test it but you'd also need to figure out how to run parts of CI/CD inside your proprietary notebook service? |
||
| ) from e | ||
|
|
||
| fabric_env_config = get_fabric_env_config().fabric_env_config | ||
| auth_header = TokenUtils().get_openai_auth_header() | ||
| return fabric_env_config, auth_header | ||
|
|
||
| def _prepare_messages(self, prompt: str | list | None, messages: list[dict[str, Any]] | None) -> list[dict]: | ||
| """Prepare messages for the API request. | ||
| Args: | ||
| prompt: A string prompt or list of messages. | ||
| messages: A list of message dictionaries. | ||
| Returns: | ||
| list: Formatted messages for the API. | ||
| """ | ||
| if messages is not None: | ||
| return messages | ||
| elif prompt is not None: | ||
| if isinstance(prompt, str): | ||
| return [{"role": "user", "content": prompt}] | ||
| elif isinstance(prompt, list): | ||
| return prompt | ||
| else: | ||
| return [{"role": "user", "content": str(prompt)}] | ||
| else: | ||
| return [] | ||
|
|
||
| def _build_payload(self, messages: list[dict], **kwargs) -> dict: | ||
| """Build the request payload based on model type. | ||
| Args: | ||
| messages: The formatted messages. | ||
| **kwargs: Additional parameters like max_tokens, temperature, etc. | ||
| Returns: | ||
| dict: The request payload. | ||
| """ | ||
| max_tokens_value = kwargs.get("max_tokens", self.kwargs.get("max_tokens", 4000)) | ||
|
|
||
| # Build payload based on model type | ||
| payload = {"messages": messages} | ||
|
|
||
| if self.is_reasoning_model: | ||
| # Reasoning models use max_completion_tokens and don't support temperature | ||
| payload["max_completion_tokens"] = max_tokens_value | ||
| # Don't include temperature or n for reasoning models | ||
| else: | ||
| # Standard models use max_tokens and support temperature | ||
| payload["max_tokens"] = max_tokens_value | ||
| payload["temperature"] = kwargs.get("temperature", self.kwargs.get("temperature", 0.0)) | ||
| payload["n"] = kwargs.get("n", 1) | ||
|
|
||
| return payload | ||
|
|
||
| def _make_request(self, payload: dict) -> list[str]: | ||
| """Make the API request to Azure OpenAI. | ||
| Args: | ||
| payload: The request payload. | ||
| Returns: | ||
| list: List of response contents. | ||
| Raises: | ||
| Exception: If the API call fails. | ||
| """ | ||
| fabric_env_config, auth_header = self._get_fabric_config() | ||
|
|
||
| url = ( | ||
| f"{fabric_env_config.ml_workload_endpoint}cognitive/openai/openai/deployments/" | ||
| f"{self.deployment_name}/chat/completions?api-version=2025-04-01-preview" | ||
| ) | ||
| headers = {"Authorization": auth_header, "Content-Type": "application/json"} | ||
|
|
||
| response = requests.post(url, headers=headers, json=payload, timeout=60) | ||
|
|
||
| if response.status_code == 200: | ||
| response_data = response.json() | ||
| return [choice["message"]["content"] for choice in response_data.get("choices", [])] | ||
| else: | ||
| raise Exception(f"API call failed: {response.status_code} - {response.text}") | ||
|
|
||
| def basic_request(self, prompt: str | list | None = None, **kwargs) -> list[str]: | ||
| """Make a basic request to the Azure OpenAI API. | ||
| Args: | ||
| prompt: The prompt string or list of messages. | ||
| **kwargs: Additional parameters for the request. | ||
| Returns: | ||
| list: List of generated response strings. | ||
| """ | ||
| messages = self._prepare_messages(prompt, None) | ||
| payload = self._build_payload(messages, **kwargs) | ||
| return self._make_request(payload) | ||
|
|
||
| def forward( | ||
| self, | ||
| prompt: str | None = None, | ||
| messages: list[dict[str, Any]] | None = None, | ||
| **kwargs, | ||
| ): | ||
| """Forward pass for the language model. | ||
| This method is required by BaseLM and must return a response in OpenAI format. | ||
| Args: | ||
| prompt: Optional string prompt. | ||
| messages: Optional list of message dictionaries. | ||
| **kwargs: Additional parameters. | ||
| Returns: | ||
| A response object compatible with OpenAI's response format. | ||
| Raises: | ||
| ValueError: If neither prompt nor messages is provided. | ||
| """ | ||
| if prompt is None and messages is None: | ||
| raise ValueError("Either 'prompt' or 'messages' must be provided") | ||
|
|
||
| # Prepare messages | ||
| formatted_messages = self._prepare_messages(prompt, messages) | ||
|
|
||
| # Build payload | ||
| payload = self._build_payload(formatted_messages, **kwargs) | ||
|
|
||
| # Make request | ||
| response_contents = self._make_request(payload) | ||
|
|
||
| # Convert to OpenAI-compatible format | ||
| # We need to return a response object that looks like OpenAI's ChatCompletion | ||
| from types import SimpleNamespace | ||
|
|
||
| choices = [ | ||
| SimpleNamespace( | ||
| message=SimpleNamespace(content=content, role="assistant"), | ||
| finish_reason="stop", | ||
| index=i, | ||
| ) | ||
| for i, content in enumerate(response_contents) | ||
| ] | ||
|
|
||
| # Create a minimal response object | ||
| response = SimpleNamespace( | ||
| choices=choices, | ||
| model=self.deployment_name, | ||
| usage=SimpleNamespace( | ||
| prompt_tokens=0, # Fabric API doesn't return token counts | ||
| completion_tokens=0, | ||
| total_tokens=0, | ||
| ), | ||
| ) | ||
|
|
||
| return response | ||
|
|
||
| def __call__( | ||
| self, | ||
| prompt: str | None = None, | ||
| messages: list[dict[str, Any]] | None = None, | ||
| **kwargs, | ||
| ) -> list[str]: | ||
| """Call the language model. | ||
| This method provides a simpler interface that returns just the text outputs. | ||
| Args: | ||
| prompt: Optional string prompt. | ||
| messages: Optional list of message dictionaries. | ||
| **kwargs: Additional parameters. | ||
| Returns: | ||
| list: List of generated response strings. | ||
| Raises: | ||
| ValueError: If neither prompt nor messages is provided. | ||
| """ | ||
| if messages is not None: | ||
| return self.basic_request(messages, **kwargs) | ||
| elif prompt is not None: | ||
| return self.basic_request(prompt, **kwargs) | ||
| else: | ||
| raise ValueError("Either 'prompt' or 'messages' must be provided") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,37 @@ class LM(BaseLM): | |
| A language model supporting chat or text completion requests for use with DSPy modules. | ||
| """ | ||
|
|
||
| def __new__( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this was the right way to do it you wouldn't have had to define new to add this. No other client has done this so why should this client be an exception? |
||
| cls, | ||
| model: str, | ||
| model_type: Literal["chat", "text", "responses"] = "chat", | ||
| temperature: float | None = None, | ||
| max_tokens: int | None = None, | ||
| cache: bool = True, | ||
| **kwargs, | ||
| ): | ||
| """Create a new LM instance, delegating to FabricAzureOpenAI for microsoftfabric/ models.""" | ||
| # Only check for microsoftfabric prefix if model is a string | ||
| if isinstance(model, str) and model.startswith("microsoftfabric/"): | ||
| # Import here to avoid circular dependency | ||
| from dspy.clients.fabric_azure_openai import FabricAzureOpenAI | ||
|
|
||
| # Extract the deployment name (everything after "microsoftfabric/") | ||
| deployment_name = model.split("microsoftfabric/", 1)[1] | ||
|
|
||
| # Create and return a FabricAzureOpenAI instance | ||
| return FabricAzureOpenAI( | ||
| deployment_name=deployment_name, | ||
| model_type=model_type, | ||
| temperature=temperature if temperature is not None else 0.0, | ||
| max_tokens=max_tokens if max_tokens is not None else 4000, | ||
| cache=cache, | ||
| **kwargs, | ||
| ) | ||
|
|
||
| # For all other models, create a regular LM instance | ||
| return super().__new__(cls) | ||
|
|
||
| def __init__( | ||
| self, | ||
| model: str, | ||
|
|
@@ -129,12 +160,7 @@ def _get_cached_completion_fn(self, completion_fn, cache): | |
|
|
||
| return completion_fn, litellm_cache_args | ||
|
|
||
| def forward( | ||
| self, | ||
| prompt: str | None = None, | ||
| messages: list[dict[str, Any]] | None = None, | ||
| **kwargs | ||
| ): | ||
| def forward(self, prompt: str | None = None, messages: list[dict[str, Any]] | None = None, **kwargs): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. formatting only change. Why mess with this? |
||
| # Build the request. | ||
| kwargs = dict(kwargs) | ||
| cache = kwargs.pop("cache", self.cache) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing information about requirement to install additional packages. And why add the advertisement for your service?