Skip to content
This repository has been archived by the owner on Aug 13, 2024. It is now read-only.

Commit

Permalink
Add OpenAPI support & example (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
ricklamers authored Dec 22, 2023
1 parent 3486986 commit 3522cbb
Show file tree
Hide file tree
Showing 9 changed files with 1,242 additions and 1,055 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ async def _get_entity_configs() -> dict[str, EntityConfig]:

find_email_by_name_function = PythonCallableFunction(
id="find_email",
type="FindEmailFunction",
display_name="Find Email",
description="Find an email address",
sample_questions=[
Expand Down
10 changes: 8 additions & 2 deletions examples/fast-api-server/fast_api_server/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import FastAPI, status
from fastapi.middleware.cors import CORSMiddleware
from openassistants.core.assistant import Assistant
from openassistants.functions.crud import PythonCRUD
from openassistants.functions.crud import OpenAPICRUD, PythonCRUD
from openassistants_fastapi import RouteAssistants, create_router

from fast_api_server.find_email_by_name_function import find_email_by_name_function
Expand All @@ -11,8 +11,14 @@
# create a library with the custom function
custom_python_lib = PythonCRUD(functions=[find_email_by_name_function])

openapi_lib = OpenAPICRUD(
spec="https://petstore3.swagger.io/api/v3/openapi.json",
base_url="https://petstore3.swagger.io/api/v3",
)


hooli_assistant = Assistant(
libraries=["piedpiper", custom_python_lib],
libraries=["piedpiper", custom_python_lib, openapi_lib],
scope_description="""Only answer questions about Hooli company related matters.
You're also allowed to answer questions that refer to anything in the current chat history.""", # noqa: E501
)
Expand Down
1,021 changes: 531 additions & 490 deletions examples/fast-api-server/poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/openassistants-fastapi/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ python = ">=3.10,<3.12"
sse-starlette = "^1.8.2"
fastapi = "^0.104.1"
starlette = ">=0.27.0,<0.28.0"
langchain = "^0.0.346"
langchain = "^0.0.352"

[tool.poetry.group.test.dependencies]
ruff = "^0.1.7"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Awaitable, Callable, Mapping, Sequence
from typing import Awaitable, Callable, Literal, Mapping, Optional, Sequence

from openassistants.data_models.function_output import FunctionOutput
from openassistants.functions.base import (
Expand All @@ -10,11 +10,14 @@


class PythonCallableFunction(BaseFunction):
type: Literal["PythonCallableFunction"] = "PythonCallableFunction"
execute_callable: Callable[
[FunctionExecutionDependency], AsyncStreamVersion[Sequence[FunctionOutput]]
]

get_entity_configs_callable: Callable[[], Awaitable[Mapping[str, IEntityConfig]]]
get_entity_configs_callable: Optional[
Callable[[], Awaitable[Mapping[str, IEntityConfig]]]
] = None

async def execute(
self, deps: FunctionExecutionDependency
Expand All @@ -23,4 +26,6 @@ async def execute(
yield output

async def get_entity_configs(self) -> Mapping[str, IEntityConfig]:
if self.get_entity_configs_callable is None:
return {}
return await self.get_entity_configs_callable()
84 changes: 80 additions & 4 deletions packages/openassistants/openassistants/functions/crud.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import abc
import asyncio
import json
from json.decoder import JSONDecodeError
from pathlib import Path
from typing import Annotated, List, Optional
from typing import Annotated, Any, Callable, Dict, List, Optional, Tuple, Union

from langchain.chains.openai_functions.openapi import openapi_spec_to_openai_fn
from langchain_community.utilities.openapi import OpenAPISpec
from openassistants.contrib.advisor_function import AdvisorFunction
from openassistants.contrib.duckdb_query import DuckDBQueryFunction
from openassistants.contrib.langchain_ddg_tool import DuckDuckGoToolFunction
from openassistants.contrib.python_callable import PythonCallableFunction
from openassistants.contrib.python_eval import PythonEvalFunction
from openassistants.contrib.sqlalchemy_query import QueryFunction
from openassistants.contrib.text_response import TextResponseFunction
from openassistants.functions.base import BaseFunction, IBaseFunction
from openassistants.utils import yaml
from openassistants.data_models.function_output import TextOutput
from openassistants.data_models.json_schema import JSONSchema
from openassistants.functions.base import (
BaseFunction,
BaseFunctionParameters,
IBaseFunction,
)
from openassistants.utils import yaml as yaml_utils
from pydantic import Field, TypeAdapter
from starlette.concurrency import run_in_threadpool

Expand Down Expand Up @@ -54,7 +65,7 @@ def read(self, function_id: str) -> Optional[BaseFunction]:
try:
if (yaml_file := self.directory / f"{function_id}.yaml").exists():
with yaml_file.open() as f:
parsed_yaml = yaml.load(f)
parsed_yaml = yaml_utils.load(f)
return TypeAdapter(AllFunctionTypes).validate_python(
parsed_yaml | {"id": function_id}
) # type: ignore
Expand Down Expand Up @@ -86,3 +97,68 @@ def read(self, slug: str) -> Optional[IBaseFunction]:

def list_ids(self) -> List[str]:
return [function.get_id() for function in self.functions]


class OpenAPICRUD(PythonCRUD):
openapi: OpenAPISpec

@staticmethod
def openai_fns_to_openapi_function(
fns: Tuple[List[Dict[str, Any]], Callable],
) -> List[PythonCallableFunction]:
openapi_functions = []
callable_fn = fns[1]

for function_schema in fns[0]:

async def wrapped_fn(deps, fs=function_schema):
response = callable_fn(fs["name"], fn_args=deps.arguments)
if response.headers.get("Content-Type") == "application/json":
try:
response_json = response.json()
yield [
TextOutput(
text="```json\n"
+ json.dumps(response_json, indent=2)
+ "\n```"
)
]
except JSONDecodeError:
yield [TextOutput(text=response.text)]
else:
yield [TextOutput(text=response.text)]

parameters = function_schema["parameters"]
if "required" not in parameters:
parameters["required"] = []

openapi_functions.append(
PythonCallableFunction(
id=function_schema["name"],
display_name=function_schema["name"],
description=function_schema["description"],
parameters=BaseFunctionParameters(
json_schema=TypeAdapter(JSONSchema).validate_python(parameters)
),
confirm=True,
execute_callable=wrapped_fn,
)
)

return openapi_functions

def __init__(self, spec: Union[OpenAPISpec, str], base_url: Optional[str]):
if isinstance(spec, str):
self.openapi = OpenAPISpec.from_url(spec)
else:
self.openapi = spec

if base_url is not None:
if self.openapi.servers is None:
self.openapi.servers = []
self.openapi.servers[0].url = base_url

openai_functions = openapi_spec_to_openai_fn(self.openapi)
functions = OpenAPICRUD.openai_fns_to_openapi_function(openai_functions)

super().__init__(functions)
2 changes: 2 additions & 0 deletions packages/openassistants/openassistants/utils/json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
class Node(BaseModel):
@staticmethod
def build(json_schema: dict):
if "type" not in json_schema:
return ObjectNode(children={})
if json_schema["type"] == "object":
return ObjectNode(
children={
Expand Down
Loading

0 comments on commit 3522cbb

Please sign in to comment.