microsoft / semantic-kernel

Integrate cutting-edge LLM technology quickly and easily into your apps
https://aka.ms/semantic-kernel
MIT License
20.36k stars 2.96k forks source link

Python: Bug: ‎Python's SessionsPythonTool is using wrong REST data and endpoints for Azure Container Apps Sessions #6934

Open colincmac opened 1 week ago

colincmac commented 1 week ago

Describe the bug The request and response bodies for interacting with the ACA serverless code interpreter sessions is incorrect.

To Reproduce Steps to reproduce the behavior:

  1. Go to the sample workbook 'azure_python_code_interpreter_function_calling'
  2. Try using the file upload and/or code execution functions
  3. See errors

Expected behavior To be able to execute code interpreter sessions in ACA

Platform

Additional context REST API specifications: https://learn.microsoft.com/en-us/azure/container-apps/sessions-code-interpreter

For example: The File Upload API endpoint should be files/upload not python/uploadFile. It also returns a list of files, rather than a single file metadata. method: https://github.com/microsoft/semantic-kernel/blob/8fc645d1fe152a53420db484ed85afc7cce903cb/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py#L177

I believe all endpoints and data are incorrect, but haven't tested the all of the plugin's functions.

markwallace-microsoft commented 1 week ago

@colincmac Thanks for the question. What version of Semantic Kernel are you using?

colincmac commented 6 days ago

@markwallace-microsoft I'm using semantic-kernel v1.1.1

taitaitai1777 commented 1 day ago

Step-by-Step Solution and Enhancement

Step 1: Identify the Incorrect REST Data and Endpoints

Examine the current SessionsPythonTool implementation and identify where the REST calls are made. Take note of the endpoints and payloads being used.

Step 2: Verify Correct REST Endpoints and Data Requirements

Refer to the Azure Container Apps documentation to verify the correct REST endpoints and data schema necessary for managing sessions.

Step 3: Configuration File Creation

Create a config.json file to store your configuration, including REST endpoints and tokens. This approach minimizes hardcoding and enhances maintainability.

config.json:

{
    "azure": {
        "create_session_endpoint": "https://<your-container-app>.azurecontainerapps.io/api/sessions",
        "access_token": "<your-access-token>"
    }
}

Step 4: Update REST Endpoints and Data Payloads

Update the SessionsPythonTool class to use the correct endpoints and payloads from the configuration file.

SessionsPythonTool:

import requests
import json
import logging

class SessionsPythonTool:
    def __init__(self):
        with open('config.json') as config_file:
            self.config = json.load(config_file)

        logging.basicConfig(filename='app.log',
                            level=logging.DEBUG,
                            filemode='w',
                            format='%(name)s - %(levelname)s - %(message)s')

    def create_session(self, session_data):
        url = self.config['azure']['create_session_endpoint']
        headers = {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + self.config['azure']['access_token']
        }

        self._log_request("POST", url, headers, session_data)

        try:
            response = requests.post(url, headers=headers, json=session_data)
            response.raise_for_status()
            self._log_response(response)
            return response.json()
        except requests.exceptions.RequestException as e:
            logging.error(f'Failed to create session: {e}')
            raise Exception(f'Error: {e}')

    def _log_request(self, method, url, headers, data):
        logging.debug(f'Request method: {method}')
        logging.debug(f'Request URL: {url}')
        logging.debug(f'Request headers: {headers}')
        logging.debug(f'Request data: {data}')

    def _log_response(self, response):
        logging.debug(f'Response status code: {response.status_code}')
        logging.debug(f'Response content: {response.content}')

# Example usage
if __name__ == "__main__":
    tool = SessionsPythonTool()
    session_data = {
        "name": "new_session",
        "config": {
            "image": "my-container-image",
            "resources": {
                "cpu": "0.5",
                "memory": "1.0Gi"
            }
        }
    }
    response = tool.create_session(session_data)
    print(response)

Step 5: Implement Data Validation

Ensure that the data sent to the REST API is validated against the expected schema, using JSON schema validation or custom functions.

JSON Schema Validation Example:

import jsonschema

SESSION_DATA_SCHEMA = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "config": {
            "type": "object",
            "properties": {
                "image": {"type": "string"},
                "resources": {
                    "type": "object",
                    "properties": {
                        "cpu": {"type": "number"},
                        "memory": {"type": "string"}
                    },
                    "required": ["cpu", "memory"]
                }
            },
            "required": ["image", "resources"]
        }
    },
    "required": ["name", "config"]
}

class SessionsPythonTool:
    # ... (other methods)

    def create_session(self, session_data):
        url = self.config['azure']['create_session_endpoint']
        headers = {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + self.config['azure']['access_token']
        }

        self._log_request("POST", url, headers, session_data)

        try:
            jsonschema.validate(instance=session_data, schema=SESSION_DATA_SCHEMA)
        except jsonschema.exceptions.ValidationError as e:
            logging.error(f'Session data validation error: {e}')
            raise ValueError(f'Session data validation error: {e}')

        try:
            response = requests.post(url, headers=headers, json=session_data)
            response.raise_for_status()
            self._log_response(response)
            return response.json()
        except requests.exceptions.RequestException as e:
            logging.error(f'Failed to create session: {e}')
            raise Exception(f'Error: {e}')

Step 6: Add Unit Tests

Use a testing framework like unittest or pytest to validate the functionality of the SessionsPythonTool.

Unit Test Example using unittest:

import unittest
from unittest.mock import patch
from sessions_python_tool import SessionsPythonTool

class TestSessionsPythonTool(unittest.TestCase):

    @patch('sessions_python_tool.requests.post')
    def test_create_session_success(self, mock_post):
        mock_post.return_value.status_code = 201
        mock_post.return_value.json.return_value = {'session_id': '12345'}

        tool = SessionsPythonTool()
        session_data = {
            "name": "test_session",
            "config": {
                "image": "test-image",
                "resources": {
                    "cpu": 0.5,
                    "memory": "1.0Gi"
                }
            }
        }
        response = tool.create_session(session_data)

        self.assertEqual(response['session_id'], '12345')
        mock_post.assert_called_once()

    @patch('sessions_python_tool.requests.post')
    def test_create_session_validation_error(self, mock_post):
        tool = SessionsPythonTool()
        invalid_session_data = {
            "name": "test_session"
            # Missing required 'config' field
        }

        with self.assertRaises(ValueError):
            tool.create_session(invalid_session_data)

    @patch('sessions_python_tool.requests.post')
    def test_create_session_failure(self, mock_post):
        mock_post.return_value.status_code = 400
        mock_post.return_value.text = 'Bad Request'

        tool = SessionsPythonTool()
        session_data = {
            "name": "test_session",
            "config": {
                "image": "test-image",
                "resources": {
                    "cpu": 0.5,
                    "memory": "1.0Gi"
                }
            }
        }

        with self.assertRaises(Exception) as context:
            tool.create_session(session_data)

        self.assertTrue('Error: 400' in str(context.exception))

if __name__ == '__main__':
    unittest.main()

Summary

This solution outlines the steps to correct the issues with the SessionsPythonTool, including configuration enhancement, proper endpoint usage, data validation, error handling, and comprehensive unit testing. These steps ensure that the tool functions correctly and is easy to maintain. By following this structured approach, developers can confidently manage Azure Container Apps Sessions.

moonbox3 commented 1 day ago

Hi @colincmac. I am not able to reproduce any issue with this SessionsPython plugin. What region did you deploy your sessions pool resource to?

When I make a call to my pool deployed to EastUS and the endpoint like <pool_endpoint>/code/execute I get a 400. If I send a request to <pool_endpoint>/python/execute I get a 200.

Similarly, if I upload a file to the container via <pool_endpoint>/python/uploadFile I get a 200, versus trying to upload via <pool_endpoint>/files/upload I get a 400.

colincmac commented 16 hours ago

I'm deployed to EastUS. I did incorporate it into an existing solution. Let me try a fresh project. It does seem like there's a difference with the docs and the current solution. For example, semantic kernel is requesting a token for the resource https://acasessions.io/.default and the docs/python quick starts request https://dynamicsessions.io/.default

Here's a Postman collection showing it work with the dynamicsessions scope Dynamic Sessions API.postman_collection.json

If I get a sec, I'll try to reproduce it via a fresh example.

taitaitai1777 commented 16 hours ago

Title: Mastering Azure Resource Token Requests: Expert Guidance

Introduction:

When integrating Azure resources into your solution, particularly in scenarios like deploying to EastUS, you might encounter discrepancies with resource token scopes. For instance, you may find that the Semantic Kernel requests a token for https://acasessions.io/.default, while documentation and Python quickstarts refer to https://dynamicsessions.io/.default. This expert guide will walk you through the process of understanding, verifying, and implementing the correct token scopes, using state-of-the-art teaching methods with detailed, step-by-step instructions.

Objective:

By the end of this guide, you will be adept at identifying and resolving scope mismatches and confidently implementing token requests that function seamlessly in your Azure-deployed applications.

Step-by-Step Instructions:

Step 1: Understanding the Scope Requirement

First, it's crucial to understand what a "scope" is and why it's important.

  1. Scope Definition: In OAuth 2.0, a scope defines what access levels are requested from the user or resource.
  2. Common Issue: Mismatched scopes can lead to authorization errors.

Step 2: Verify with Postman Collection

Validating your scope using Postman ensures correctness before integrating into your code.

  1. Download Postman: If you haven't already, download and install Postman.
  2. Import Collection:
    • Open Postman and click the "Import" button.
    • Select Dynamic Sessions API.postman_collection.json from your files.
  3. Configure Request:
    • Select the imported collection and navigate to the request for acquiring a token.
    • Update the required fields (Client ID, Client Secret, Tenant ID) with your Azure credentials.
    • Set the scope to https://dynamicsessions.io/.default.
  4. Send Request:
    • Click "Send" and observe the response.
    • Ensure you receive a valid token without errors.

Step 3: Implement in Your Application

Now that we have verified the correct scope using Postman, we implement this in our application.

  1. Python Setup:
    • Ensure you have Python installed on your machine. If not, download it from Python.org.
    • Install the necessary Python packages using pip:
      pip install requests
  2. Code Implementation:

    • Copy and paste the following code into your Python script, updating the placeholders with your actual Azure credentials.
      
      import requests
      import json

    Function to request token with given scope

    def get_token(tenant_id, client_id, client_secret, scope): token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token" headers = {'Content-Type': 'application/x-www-form-urlencoded'} body = { 'grant_type': 'client_credentials', 'client_id': client_id, 'client_secret': client_secret, 'scope': scope } response = requests.post(token_url, headers=headers, data=body) token_response = response.json()

    if response.status_code == 200: return token_response['access_token'] else: raise Exception(f"Failed to acquire token: {token_response}")

    Example usage

    tenant_id = 'YOUR_TENANT_ID' client_id = 'YOUR_CLIENT_ID' client_secret = 'YOUR_CLIENT_SECRET' scope = 'https://dynamicsessions.io/.default' # Ensure this matches the verified scope

    try: token = get_token(tenant_id, client_id, client_secret, scope) print(f"Token acquired: {token}") except Exception as e: print(str(e))

Step 4: Validate and Troubleshoot

  1. Run Your Script:
    • Execute your Python script to check if the token is acquired successfully.
  2. Troubleshooting:
    • If you encounter any errors, carefully read the error message for clues.
    • Common issues include incorrect credentials, misconfigured Azure app registrations, and network problems.

Step 5: Additional Validation in Azure Portal

  1. Azure Portal Checks:
    • Log into the Azure Portal.
    • Go to your Azure AD App registrations.
    • Verify the API permissions and ensure that consented scopes match the ones you're requesting in your application.
  2. Consent to Scopes:
    • Make sure the necessary scopes have admin consent granted.

Conclusion:

Following this detailed, step-by-step guide, you've learned the importance of correctly matching token scopes, verified scopes using Postman, implemented them in a Python application, and validated their correctness. This expert approach ensures robust and error-free integration of Azure resources into your solution.

moonbox3 commented 11 hours ago

@colincmac, thanks for your response. I have it working with the endpoints listed in the documentation that you linked to. I still find it odd that it works with the old endpoints/request body as well. I've linked the PR above.

taitaitai1777 commented 9 hours ago
// Explanation of Observations Regarding API Endpoints
//
// It's great to hear that you have successfully integrated the new endpoints
// listed in the documentation! You've noted that the old endpoints and request
// bodies are still functioning, which may seem odd at first. Let's break down
// why this might be the case:

/*
1. Backward Compatibility:
   - To ensure existing applications continue to work without interruption,
     many API providers maintain backward compatibility.
   - This means that old endpoints and request bodies are often still supported
     even after new versions are released.

2. Deprecation Policy:
   - API providers typically deprecate endpoints gradually.
   - They might support old endpoints for a transitional period while notifying
     developers that these endpoints will be phased out in the future.
   - Both old and new endpoints may work concurrently during this period, but
     developers are encouraged to migrate to the new endpoints.

3. Incremental Migration:
   - Organizations may roll out new features or endpoints incrementally.
   - This allows developers to test and integrate the new endpoints without
     disrupting existing systems immediately.

4. Versioning:
   - APIs often use versioning to allow different versions to coexist.
   - For instance, `/api/v1/resource` might still be supported alongside
     `/api/v2/resource`.
   - You may be using different versions without realizing it, which explains
     why both old and new endpoints work.

Observation Summary:
- Your observation suggests that the API provider aims for a smooth transition
  and avoids breaking changes for existing users.
- This is a common practice to help developers update their integrations at a
  manageable pace.

Next Steps:
- Check Documentation:
  - Review the API documentation for any deprecation notices or timelines
    indicating when old endpoints will be discontinued.
- Update Code:
  - Gradually update your codebase to use the new endpoints and request bodies
    to future-proof your application.
- Test Thoroughly:
  - Ensure all functionalities work as expected with the new endpoints and
    request bodies.
- Monitor Announcements:
  - Stay updated on official announcements from the API provider regarding any
    changes or updates.

Conclusion:
- You're on the right track by using the new endpoints, which will make your
  application more robust against future changes.
- If you have any specific concerns or need further assistance, feel free to
  share more details, and additional support can be provided.
*/