Skip to content

Commit f5d43a8

Browse files
feat: function categories (#24)
Co-authored-by: afernand <[email protected]>
1 parent d361ada commit f5d43a8

File tree

7 files changed

+180
-3
lines changed

7 files changed

+180
-3
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Allie FlowKit Python can be run locally or as a Docker container. Follow the ins
9696
- Add your function code as an endpoint to a new Python file in the `allie/flowkit/endpoints` directory.
9797
- Use the `allie/flowkit/endpoints/splitter.py` file and its endpoints as an example.
9898
- Explicitly define the input and output of the function using Pydantic models, as these will be used by the Allie Agent to call the function.
99+
- Add the category and display name of the function to the endpoint definition.
99100

100101
2. **Add the models for the function:**
101102
- Create the models for the input and output of the function in the `allie/flowkit/models` directory.
@@ -147,7 +148,7 @@ Allie FlowKit Python can be run locally or as a Docker container. Follow the ins
147148
```
148149

149150
3. **Define your custom function:**
150-
- Add your function to ``custom_endpoint.py``, explicitly defining the input and output using Pydantic models.
151+
- Add your function to ``custom_endpoint.py``, explicitly defining the input and output using Pydantic models, and the category and display name of the function.
151152

152153
**custom_endpoint.py**:
153154
```python
@@ -156,6 +157,8 @@ Allie FlowKit Python can be run locally or as a Docker container. Follow the ins
156157
157158
158159
@router.post("/custom_function", response_model=CustomResponse)
160+
@category(FunctionCategory.GENERIC)
161+
@display_name("Custom Function")
159162
async def custom_function(request: CustomRequest) -> CustomResponse:
160163
"""Endpoint for custom function.
161164

src/allie/flowkit/endpoints/splitter.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import io
2727

2828
from allie.flowkit.config._config import CONFIG
29+
from allie.flowkit.models.functions import FunctionCategory
2930
from allie.flowkit.models.splitter import SplitterRequest, SplitterResponse
31+
from allie.flowkit.utils.decorators import category, display_name
3032
from fastapi import APIRouter, Header, HTTPException
3133
from langchain.text_splitter import PythonCodeTextSplitter, RecursiveCharacterTextSplitter
3234
from pdfminer.high_level import extract_text
@@ -38,6 +40,8 @@
3840

3941

4042
@router.post("/ppt", response_model=SplitterResponse)
43+
@category(FunctionCategory.DATA_EXTRACTION)
44+
@display_name("Split PPT")
4145
async def split_ppt(request: SplitterRequest, api_key: str = Header(...)) -> SplitterResponse:
4246
"""Endpoint for splitting text in a PowerPoint document into chunks.
4347
@@ -55,6 +59,8 @@ async def split_ppt(request: SplitterRequest, api_key: str = Header(...)) -> Spl
5559

5660

5761
@router.post("/py", response_model=SplitterResponse)
62+
@category(FunctionCategory.DATA_EXTRACTION)
63+
@display_name("Split Python Code")
5864
async def split_py(request: SplitterRequest, api_key: str = Header(...)) -> SplitterResponse:
5965
"""Endpoint for splitting Python code into chunks.
6066
@@ -77,6 +83,8 @@ async def split_py(request: SplitterRequest, api_key: str = Header(...)) -> Spli
7783

7884

7985
@router.post("/pdf", response_model=SplitterResponse)
86+
@category(FunctionCategory.DATA_EXTRACTION)
87+
@display_name("Split PDF")
8088
async def split_pdf(request: SplitterRequest, api_key: str = Header(...)) -> SplitterResponse:
8189
"""Endpoint for splitting text in a PDF document into chunks.
8290

src/allie/flowkit/fastapi_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,22 @@ def extract_endpoint_info(function_map: dict[str, Any], routes: list[APIRoute])
222222
output_definitions = get_definitions_from_return_type(return_type) if return_type else {}
223223
definitions = {**input_definitions, **output_definitions}
224224

225+
# Extract category and display name from decorators
226+
category = getattr(route.endpoint, "category", "Uncategorized")
227+
display_name = getattr(route.endpoint, "display_name", func_name)
228+
229+
# Extract the description from the docstring
230+
description = inspect.getdoc(route.endpoint) or "No description available"
231+
225232
endpoint_info = EndpointInfo(
226233
name=func_name,
227234
path=route.path,
228235
inputs=inputs,
229236
outputs=outputs,
230237
definitions=definitions,
238+
category=category,
239+
display_name=display_name,
240+
description=description,
231241
)
232242
endpoint_list.append(endpoint_info)
233243
return endpoint_list

src/allie/flowkit/models/functions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
"""Module for defining the models used in the endpoints."""
2424

25+
from enum import Enum
2526
from typing import Any
2627

2728
from pydantic import BaseModel
@@ -53,6 +54,19 @@ class EndpointInfo(BaseModel):
5354

5455
name: str
5556
path: str
57+
category: str
58+
display_name: str
59+
description: str
5660
inputs: list[ParameterInfo]
5761
outputs: list[ParameterInfo]
5862
definitions: dict[str, Any]
63+
64+
65+
class FunctionCategory(Enum):
66+
"""Enum for function categories."""
67+
68+
DATA_EXTRACTION = "data_extraction"
69+
GENERIC = "generic"
70+
KNOWLEDGE_DB = "knowledge_db"
71+
LLM_HANDLER = "llm_handler"
72+
ANSYS_GPT = "ansys_gpt"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
"""Utils module."""
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Decorators module for function definitions."""
24+
25+
import asyncio
26+
from functools import wraps
27+
28+
29+
def category(value: str):
30+
"""Decorator to add a category to the function."""
31+
32+
def decorator(func):
33+
func.category = value
34+
35+
@wraps(func)
36+
async def async_wrapper(*args, **kwargs):
37+
# Check if function is async
38+
if asyncio.iscoroutinefunction(func):
39+
return await func(*args, **kwargs)
40+
else:
41+
return func(*args, **kwargs)
42+
43+
return async_wrapper
44+
45+
return decorator
46+
47+
48+
def display_name(value: str):
49+
"""Decorator to add a display name to the function."""
50+
51+
def decorator(func):
52+
func.display_name = value
53+
54+
@wraps(func)
55+
async def async_wrapper(*args, **kwargs):
56+
# Check if function is async
57+
if asyncio.iscoroutinefunction(func):
58+
return await func(*args, **kwargs)
59+
else:
60+
return func(*args, **kwargs)
61+
62+
return async_wrapper
63+
64+
return decorator

tests/test_list_functions.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
# SOFTWARE.
2222
"""Test module for list functions."""
2323

24+
import re
25+
2426
from allie.flowkit import flowkit_service
2527
from fastapi.testclient import TestClient
2628
import pytest
@@ -29,10 +31,22 @@
2931
client = TestClient(flowkit_service)
3032

3133

34+
def normalize_text(text):
35+
"""Remove extra spaces, newlines, and indentation."""
36+
return re.sub(r"\s+", " ", text.strip())
37+
38+
39+
def normalize_response_data(data):
40+
"""Normalize descriptions in the list of functions."""
41+
for item in data:
42+
if "description" in item:
43+
item["description"] = normalize_text(item["description"])
44+
return data
45+
46+
3247
@pytest.mark.asyncio
3348
async def test_list_functions():
3449
"""Test listing available functions."""
35-
# Test splitter results
3650
response = client.get("/", headers={"api-key": "test_api_key"})
3751
assert response.status_code == 200
3852
response_data = response.json()
@@ -41,6 +55,16 @@ async def test_list_functions():
4155
{
4256
"name": "split_ppt",
4357
"path": "/splitter/ppt",
58+
"category": "data_extraction",
59+
"display_name": "Split PPT",
60+
"description": """Endpoint for splitting text in a PowerPoint document into chunks.
61+
Parameters
62+
----------
63+
request : SplitterRequest
64+
An object containing 'document_content' in Base64,
65+
'chunk_size', and 'chunk_overlap'
66+
api_key : str
67+
The API key for authentication.""",
4468
"inputs": [
4569
{"name": "document_content", "type": "string(binary)"},
4670
{"name": "chunk_size", "type": "integer"},
@@ -52,6 +76,20 @@ async def test_list_functions():
5276
{
5377
"name": "split_py",
5478
"path": "/splitter/py",
79+
"category": "data_extraction",
80+
"display_name": "Split Python Code",
81+
"description": """Endpoint for splitting Python code into chunks.
82+
Parameters
83+
----------
84+
request : SplitterRequest
85+
An object containing 'document_content' in Base64,
86+
'chunk_size', and 'chunk_overlap'
87+
api_key : str
88+
The API key for authentication.
89+
Returns
90+
-------
91+
SplitterResponse
92+
An object containing a list of text chunks.""",
5593
"inputs": [
5694
{"name": "document_content", "type": "string(binary)"},
5795
{"name": "chunk_size", "type": "integer"},
@@ -63,6 +101,20 @@ async def test_list_functions():
63101
{
64102
"name": "split_pdf",
65103
"path": "/splitter/pdf",
104+
"category": "data_extraction",
105+
"display_name": "Split PDF",
106+
"description": """Endpoint for splitting text in a PDF document into chunks.
107+
Parameters
108+
----------
109+
request : SplitterRequest
110+
An object containing 'document_content' in Base64,
111+
'chunk_size', and 'chunk_overlap'.
112+
api_key : str
113+
The API key for authentication.
114+
Returns
115+
-------
116+
SplitterResponse
117+
An object containing a list of text chunks.""",
66118
"inputs": [
67119
{"name": "document_content", "type": "string(binary)"},
68120
{"name": "chunk_size", "type": "integer"},
@@ -73,7 +125,11 @@ async def test_list_functions():
73125
},
74126
]
75127

76-
assert response_data[:3] == expected_response_start
128+
# Normalize both actual and expected data
129+
normalized_response = normalize_response_data(response_data[:3])
130+
normalized_expected = normalize_response_data(expected_response_start)
131+
132+
assert normalized_response == normalized_expected
77133

78134
# Test invalid API key
79135
response = client.get("/", headers={"api-key": "invalid_api_key"})

0 commit comments

Comments
 (0)