Skip to content

Commit

Permalink
Python: Adding core HttpSkill (microsoft#504)
Browse files Browse the repository at this point in the history
### Motivation and Context
HttpSkill  python version


### Description
- Porting the HttpSkill from C# to Python.
- Adding corresponding unit tests.

Co-authored-by: Abby Harrison <[email protected]>
  • Loading branch information
JTremb and awharrison-28 authored Apr 21, 2023
1 parent 7d11dac commit cef3fc3
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 2 deletions.
2 changes: 1 addition & 1 deletion FEATURE_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
| TextMemorySkill ||| |
| ConversationSummarySkill ||| |
| FileIOSkill ||| |
| HttpSkill || | |
| HttpSkill || | |
| MathSkill ||| |
| TextSkill ||| |
| TimeSkill ||| |
Expand Down
3 changes: 2 additions & 1 deletion python/semantic_kernel/core_skills/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.

from semantic_kernel.core_skills.file_io_skill import FileIOSkill
from semantic_kernel.core_skills.http_skill import HttpSkill
from semantic_kernel.core_skills.text_memory_skill import TextMemorySkill
from semantic_kernel.core_skills.text_skill import TextSkill
from semantic_kernel.core_skills.time_skill import TimeSkill

__all__ = ["TextMemorySkill", "TextSkill", "FileIOSkill", "TimeSkill"]
__all__ = ["TextMemorySkill", "TextSkill", "FileIOSkill", "TimeSkill", "HttpSkill"]
106 changes: 106 additions & 0 deletions python/semantic_kernel/core_skills/http_skill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Copyright (c) Microsoft. All rights reserved.

import json

import aiohttp

from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.skill_definition import sk_function, sk_function_context_parameter


class HttpSkill:
"""
A skill that provides HTTP functionality.
Usage:
kernel.import_skill(HttpSkill(), "http")
Examples:
{{http.getAsync $url}}
{{http.postAsync $url}}
{{http.putAsync $url}}
{{http.deleteAsync $url}}
"""

@sk_function(description="Makes a GET request to a uri", name="getAsync")
async def get_async(self, url: str) -> str:
"""
Sends an HTTP GET request to the specified URI and returns
the response body as a string.
params:
uri: The URI to send the request to.
returns:
The response body as a string.
"""
if not url:
raise ValueError("url cannot be `None` or empty")

async with aiohttp.ClientSession() as session:
async with session.get(url, raise_for_status=True) as response:
return await response.text()

@sk_function(description="Makes a POST request to a uri", name="postAsync")
@sk_function_context_parameter(name="body", description="The body of the request")
async def post_async(self, url: str, context: SKContext) -> str:
"""
Sends an HTTP POST request to the specified URI and returns
the response body as a string.
params:
url: The URI to send the request to.
context: Contains the body of the request
returns:
The response body as a string.
"""
if not url:
raise ValueError("url cannot be `None` or empty")

_, body = context.variables.get("body")

headers = {"Content-Type": "application/json"}
data = json.dumps(body)
async with aiohttp.ClientSession() as session:
async with session.post(
url, headers=headers, data=data, raise_for_status=True
) as response:
return await response.text()

@sk_function(description="Makes a PUT request to a uri", name="putAsync")
@sk_function_context_parameter(name="body", description="The body of the request")
async def put_async(self, url: str, context: SKContext) -> str:
"""
Sends an HTTP PUT request to the specified URI and returns
the response body as a string.
params:
url: The URI to send the request to.
returns:
The response body as a string.
"""
if not url:
raise ValueError("url cannot be `None` or empty")

_, body = context.variables.get("body")

headers = {"Content-Type": "application/json"}
data = json.dumps(body)
async with aiohttp.ClientSession() as session:
async with session.put(
url, headers=headers, data=data, raise_for_status=True
) as response:
return await response.text()

@sk_function(description="Makes a DELETE request to a uri", name="deleteAsync")
async def delete_async(self, url: str) -> str:
"""
Sends an HTTP DELETE request to the specified URI and returns
the response body as a string.
params:
uri: The URI to send the request to.
returns:
The response body as a string.
"""
if not url:
raise ValueError("url cannot be `None` or empty")
async with aiohttp.ClientSession() as session:
async with session.delete(url, raise_for_status=True) as response:
return await response.text()
108 changes: 108 additions & 0 deletions python/tests/unit/core_skills/test_http_skill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copyright (c) Microsoft. All rights reserved.

from unittest.mock import patch

import pytest

from semantic_kernel import Kernel
from semantic_kernel.core_skills import HttpSkill
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.sk_context import SKContext


@pytest.mark.asyncio
async def test_it_can_be_instantiated():
skill = HttpSkill()
assert skill is not None


@pytest.mark.asyncio
async def test_it_can_be_imported():
kernel = Kernel()
skill = HttpSkill()
assert kernel.import_skill(skill, "http")
assert kernel.skills.has_native_function("http", "getAsync")
assert kernel.skills.has_native_function("http", "postAsync")


@patch("aiohttp.ClientSession.get")
@pytest.mark.asyncio
async def test_get(mock_get):
mock_get.return_value.__aenter__.return_value.text.return_value = "Hello"
mock_get.return_value.__aenter__.return_value.status = 200

skill = HttpSkill()
response = await skill.get_async("https://example.org/get")
assert response == "Hello"


@pytest.mark.asyncio
async def test_get_none_url():
skill = HttpSkill()
with pytest.raises(ValueError):
await skill.get_async(None)


@patch("aiohttp.ClientSession.post")
@pytest.mark.asyncio
async def test_post(mock_post):
mock_post.return_value.__aenter__.return_value.text.return_value = "Hello World !"
mock_post.return_value.__aenter__.return_value.status = 200

skill = HttpSkill()
context_variables = ContextVariables()
context_variables.set("body", "{message: 'Hello, world!'}")
context = SKContext(context_variables, None, None, None)
response = await skill.post_async("https://example.org/post", context)
assert response == "Hello World !"


@patch("aiohttp.ClientSession.post")
@pytest.mark.asyncio
async def test_post_nobody(mock_post):
mock_post.return_value.__aenter__.return_value.text.return_value = "Hello World !"
mock_post.return_value.__aenter__.return_value.status = 200

skill = HttpSkill()
context_variables = ContextVariables()
context = SKContext(context_variables, None, None, None)
response = await skill.post_async("https://example.org/post", context)
assert response == "Hello World !"


@patch("aiohttp.ClientSession.put")
@pytest.mark.asyncio
async def test_put(mock_put):
mock_put.return_value.__aenter__.return_value.text.return_value = "Hello World !"
mock_put.return_value.__aenter__.return_value.status = 200

skill = HttpSkill()
context_variables = ContextVariables()
context_variables.set("body", "{message: 'Hello, world!'}")
context = SKContext(context_variables, None, None, None)
response = await skill.put_async("https://example.org/put", context)
assert response == "Hello World !"


@patch("aiohttp.ClientSession.put")
@pytest.mark.asyncio
async def test_put_nobody(mock_put):
mock_put.return_value.__aenter__.return_value.text.return_value = "Hello World !"
mock_put.return_value.__aenter__.return_value.status = 200

skill = HttpSkill()
context_variables = ContextVariables()
context = SKContext(context_variables, None, None, None)
response = await skill.put_async("https://example.org/put", context)
assert response == "Hello World !"


@patch("aiohttp.ClientSession.delete")
@pytest.mark.asyncio
async def test_delete(mock_delete):
mock_delete.return_value.__aenter__.return_value.text.return_value = "Hello World !"
mock_delete.return_value.__aenter__.return_value.status = 200

skill = HttpSkill()
response = await skill.delete_async("https://example.org/delete")
assert response == "Hello World !"

0 comments on commit cef3fc3

Please sign in to comment.