mirror of
https://github.com/hwchase17/langchain.git
synced 2025-07-16 01:37:59 +00:00
robocorp[patch]: Fix nested arguments descriptors and tool names (#19707)
Thank you for contributing to LangChain! - [x] **PR title**: "package: description" - Where "package" is whichever of langchain, community, core, experimental, etc. is being modified. Use "docs: ..." for purely docs changes, "templates: ..." for template changes, "infra: ..." for CI changes. - Example: "community: add foobar LLM" - [x] **PR message**: - **Description:** Fix argument translation from OpenAPI spec to OpenAI function call (and similar) - **Issue:** OpenGPTs failures with calling Action Server based actions. - **Dependencies:** None - **Twitter handle:** mikkorpela - [x] **Add tests and docs**: If you're adding a new integration, please include 1. a test for the integration, preferably unit tests that do not rely on network access, ~2. an example notebook showing its use. It lives in `docs/docs/integrations` directory.~ - [x] **Lint and test**: Run `make format`, `make lint` and `make test` from the root of the package(s) you've modified. See contribution guidelines for more: https://python.langchain.com/docs/contributing/ Additional guidelines: - Make sure optional dependencies are imported within a function. - Please do not add dependencies to pyproject.toml files (even optional ones) unless they are required for unit tests. - Most PRs should not touch more than one package. - Changes should be backwards compatible. - If you are adding something to community, do not re-import it in langchain. If no one reviews your PR within a few days, please @-mention one of baskaryan, efriis, eyurtsev, hwchase17.
This commit is contained in:
parent
48f84e253e
commit
3f06cef60c
@ -1,6 +1,7 @@
|
||||
# langchain-robocorp
|
||||
|
||||
This package contains the LangChain integrations for [Robocorp](https://github.com/robocorp/robocorp).
|
||||
This package contains the LangChain integrations for [Robocorp Action Server](https://github.com/robocorp/robocorp).
|
||||
Action Server enables an agent to execute actions in the real world.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
|
||||
from langchain_core.pydantic_v1 import BaseModel, Field, create_model
|
||||
from langchain_core.utils.json_schema import dereference_refs
|
||||
|
||||
|
||||
@ -72,28 +73,6 @@ def reduce_openapi_spec(url: str, spec: dict) -> ReducedOpenAPISpec:
|
||||
)
|
||||
|
||||
|
||||
def get_required_param_descriptions(endpoint_spec: dict) -> str:
|
||||
"""Get an OpenAPI endpoint required parameter descriptions"""
|
||||
descriptions = []
|
||||
|
||||
schema = (
|
||||
endpoint_spec.get("requestBody", {})
|
||||
.get("content", {})
|
||||
.get("application/json", {})
|
||||
.get("schema", {})
|
||||
)
|
||||
properties = schema.get("properties", {})
|
||||
|
||||
required_fields = schema.get("required", [])
|
||||
|
||||
for key, value in properties.items():
|
||||
if "description" in value:
|
||||
if value.get("required") or key in required_fields:
|
||||
descriptions.append(value.get("description"))
|
||||
|
||||
return ", ".join(descriptions)
|
||||
|
||||
|
||||
type_mapping = {
|
||||
"string": str,
|
||||
"integer": int,
|
||||
@ -105,25 +84,66 @@ type_mapping = {
|
||||
}
|
||||
|
||||
|
||||
def get_param_fields(endpoint_spec: dict) -> dict:
|
||||
"""Get an OpenAPI endpoint parameter details"""
|
||||
fields = {}
|
||||
|
||||
schema = (
|
||||
def get_schema(endpoint_spec: dict) -> dict:
|
||||
return (
|
||||
endpoint_spec.get("requestBody", {})
|
||||
.get("content", {})
|
||||
.get("application/json", {})
|
||||
.get("schema", {})
|
||||
)
|
||||
|
||||
|
||||
def create_field(schema: dict, required: bool) -> Tuple[Any, Any]:
|
||||
"""
|
||||
Creates a Pydantic field based on the schema definition.
|
||||
"""
|
||||
field_type = type_mapping.get(schema.get("type", "string"), str)
|
||||
description = schema.get("description", "")
|
||||
|
||||
# Handle nested objects
|
||||
if schema["type"] == "object":
|
||||
nested_fields = {
|
||||
k: create_field(v, k in schema.get("required", []))
|
||||
for k, v in schema.get("properties", {}).items()
|
||||
}
|
||||
model_name = schema.get("title", "NestedModel")
|
||||
nested_model = create_model(model_name, **nested_fields) # type: ignore
|
||||
return nested_model, Field(... if required else None, description=description)
|
||||
|
||||
# Handle arrays
|
||||
elif schema["type"] == "array":
|
||||
item_type, _ = create_field(schema["items"], required=True)
|
||||
return List[item_type], Field( # type: ignore
|
||||
... if required else None, description=description
|
||||
)
|
||||
|
||||
# Other types
|
||||
return field_type, Field(... if required else None, description=description)
|
||||
|
||||
|
||||
def get_param_fields(endpoint_spec: dict) -> dict:
|
||||
"""Get an OpenAPI endpoint parameter details"""
|
||||
schema = get_schema(endpoint_spec)
|
||||
properties = schema.get("properties", {})
|
||||
required_fields = schema.get("required", [])
|
||||
|
||||
fields = {}
|
||||
for key, value in properties.items():
|
||||
details = {
|
||||
"description": value.get("description", ""),
|
||||
"required": key in required_fields,
|
||||
}
|
||||
field_type = type_mapping[value.get("type", "string")]
|
||||
fields[key] = (field_type, details)
|
||||
is_required = key in required_fields
|
||||
field_info = create_field(value, is_required)
|
||||
fields[key] = field_info
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def model_to_dict(
|
||||
item: Union[BaseModel, List, Dict[str, Any]],
|
||||
) -> Any:
|
||||
if isinstance(item, BaseModel):
|
||||
return item.dict()
|
||||
elif isinstance(item, dict):
|
||||
return {key: model_to_dict(value) for key, value in item.items()}
|
||||
elif isinstance(item, list):
|
||||
return [model_to_dict(element) for element in item]
|
||||
else:
|
||||
return item
|
||||
|
@ -1,11 +1,10 @@
|
||||
# flake8: noqa
|
||||
TOOLKIT_TOOL_DESCRIPTION = """{description}. The tool must be invoked with a complete sentence starting with "{name}" and additional information on {required_params}."""
|
||||
|
||||
|
||||
API_CONTROLLER_PROMPT = """You are turning user input into a json query for an API request tool.
|
||||
API_CONTROLLER_PROMPT = (
|
||||
"You are turning user input into a json query"
|
||||
""" for an API request tool.
|
||||
|
||||
The final output to the tool should be a json string with a single key "data".
|
||||
The value of "data" should be a dictionary of key-value pairs you want to POST to the url.
|
||||
The value of "data" should be a dictionary of key-value pairs you want """
|
||||
"""to POST to the url.
|
||||
Always use double quotes for strings in the json string.
|
||||
Always respond only with the json object and nothing else.
|
||||
|
||||
@ -16,3 +15,4 @@ Endpoint documentation:
|
||||
|
||||
User Input: {input}
|
||||
"""
|
||||
)
|
||||
|
@ -20,12 +20,11 @@ from langsmith import Client
|
||||
|
||||
from langchain_robocorp._common import (
|
||||
get_param_fields,
|
||||
get_required_param_descriptions,
|
||||
model_to_dict,
|
||||
reduce_openapi_spec,
|
||||
)
|
||||
from langchain_robocorp._prompts import (
|
||||
API_CONTROLLER_PROMPT,
|
||||
TOOLKIT_TOOL_DESCRIPTION,
|
||||
)
|
||||
|
||||
MAX_RESPONSE_LENGTH = 5000
|
||||
@ -156,17 +155,9 @@ class ActionServerToolkit(BaseModel):
|
||||
if not endpoint.startswith("/api/actions"):
|
||||
continue
|
||||
|
||||
summary = docs["summary"]
|
||||
|
||||
tool_description = TOOLKIT_TOOL_DESCRIPTION.format(
|
||||
name=summary,
|
||||
description=docs.get("description", summary),
|
||||
required_params=get_required_param_descriptions(docs),
|
||||
)
|
||||
|
||||
tool_args: ToolArgs = {
|
||||
"name": f"robocorp_action_server_{docs['operationId']}",
|
||||
"description": tool_description,
|
||||
"name": docs["operationId"],
|
||||
"description": docs["description"],
|
||||
"callback_manager": callback_manager,
|
||||
}
|
||||
|
||||
@ -218,16 +209,17 @@ class ActionServerToolkit(BaseModel):
|
||||
self, endpoint: str, docs: dict, tools_args: ToolArgs
|
||||
) -> BaseTool:
|
||||
fields = get_param_fields(docs)
|
||||
_DynamicToolInputSchema = create_model("DynamicToolInputSchema", **fields)
|
||||
|
||||
def create_function(endpoint: str) -> Callable:
|
||||
def func(**data: dict[str, Any]) -> str:
|
||||
return self._action_request(endpoint, **data)
|
||||
def dynamic_func(**data: dict[str, Any]) -> str:
|
||||
return self._action_request(endpoint, **model_to_dict(data))
|
||||
|
||||
return func
|
||||
dynamic_func.__name__ = tools_args["name"]
|
||||
dynamic_func.__doc__ = tools_args["description"]
|
||||
|
||||
return StructuredTool(
|
||||
func=create_function(endpoint),
|
||||
args_schema=create_model("DynamicToolInputSchema", **fields),
|
||||
func=dynamic_func,
|
||||
args_schema=_DynamicToolInputSchema,
|
||||
**tools_args,
|
||||
)
|
||||
|
||||
|
17
libs/partners/robocorp/poetry.lock
generated
17
libs/partners/robocorp/poetry.lock
generated
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@ -277,13 +277,13 @@ url = "../../core"
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.1.31"
|
||||
version = "0.1.33"
|
||||
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
files = [
|
||||
{file = "langsmith-0.1.31-py3-none-any.whl", hash = "sha256:5211a9dc00831db307eb843485a97096484b697b5d2cd1efaac34228e97ca087"},
|
||||
{file = "langsmith-0.1.31.tar.gz", hash = "sha256:efd54ccd44be7fda911bfdc0ead340473df2fdd07345c7252901834d0c4aa37e"},
|
||||
{file = "langsmith-0.1.33-py3-none-any.whl", hash = "sha256:b84642d854b8f13ab6f540bb6d1c2b0e3e897add34b6d0880f3c3682c1a657fe"},
|
||||
{file = "langsmith-0.1.33.tar.gz", hash = "sha256:d368b7817c5a871f5ef8ca73435498aec1cbe1b13419417c91a34cffa49767ad"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -589,17 +589,17 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy
|
||||
|
||||
[[package]]
|
||||
name = "pytest-mock"
|
||||
version = "3.12.0"
|
||||
version = "3.14.0"
|
||||
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"},
|
||||
{file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"},
|
||||
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
|
||||
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=5.0"
|
||||
pytest = ">=6.2.5"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||
@ -658,7 +658,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
|
@ -1,7 +1,7 @@
|
||||
[tool.poetry]
|
||||
name = "langchain-robocorp"
|
||||
version = "0.0.4"
|
||||
description = "An integration package connecting Robocorp and LangChain"
|
||||
version = "0.0.5"
|
||||
description = "An integration package connecting Robocorp Action Server and LangChain"
|
||||
authors = []
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/langchain-ai/langchain"
|
||||
|
387
libs/partners/robocorp/tests/unit_tests/_openapi2.fixture.json
Normal file
387
libs/partners/robocorp/tests/unit_tests/_openapi2.fixture.json
Normal file
@ -0,0 +1,387 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Robocorp Action Server",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://hosted-actions.onrender.com"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/actions/google-sheet-gmail/get-google-spreadsheet-schema/run": {
|
||||
"post": {
|
||||
"summary": "Get Google Spreadsheet Schema",
|
||||
"description": "Action to get necessary information to be able to work with a Google Sheet Spreadsheets correctly.\nUse this action minimum once before anything else, to learn about the structure\nof the Spreadsheet. Method will return the first few rows of each Sheet as an example.",
|
||||
"operationId": "get_google_spreadsheet_schema",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Response Get Google Spreadsheet Schema",
|
||||
"description": "Names of the sheets, and a couple of first rows from each sheet to explain the context."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/actions/google-sheet-gmail/create-new-google-sheet/run": {
|
||||
"post": {
|
||||
"summary": "Create New Google Sheet",
|
||||
"description": "Creates a new empty Sheet in user's Google Spreadsheet.",
|
||||
"operationId": "create_new_google_sheet",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name",
|
||||
"description": "Name of the Sheet. You must refer to this Sheet name later when adding or reading date from the Sheet."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Response Create New Google Sheet",
|
||||
"description": "True if operation was success, and False if it failed."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/actions/google-sheet-gmail/add-sheet-rows/run": {
|
||||
"post": {
|
||||
"summary": "Add Sheet Rows",
|
||||
"description": "Action to add multiple rows to the Google sheet. Get the sheets with get_google_spreadsheet_schema if you don't know\nthe names or data structure. Make sure the values are in correct columns (needs to be ordered the same as in the sample).\nStrictly adhere to the schema.",
|
||||
"operationId": "add_sheet_rows",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"sheet": {
|
||||
"type": "string",
|
||||
"title": "Sheet",
|
||||
"description": "Name of the sheet where the data is added to"
|
||||
},
|
||||
"rows_to_add": {
|
||||
"properties": {
|
||||
"rows": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"columns": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Columns",
|
||||
"description": "The columns that make up the row"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"columns"
|
||||
],
|
||||
"title": "Row"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Rows",
|
||||
"description": "The rows that need to be added"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"rows"
|
||||
],
|
||||
"title": "Rows To Add",
|
||||
"description": "the rows to be added to the sheet"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"sheet",
|
||||
"rows_to_add"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Response Add Sheet Rows",
|
||||
"description": "The result of the operation."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/actions/google-sheet-gmail/get-sheet-contents/run": {
|
||||
"post": {
|
||||
"summary": "Get Sheet Contents",
|
||||
"description": "Get all content from the chosen Google Spreadsheet Sheet.",
|
||||
"operationId": "get_sheet_contents",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"sheet": {
|
||||
"type": "string",
|
||||
"title": "Sheet",
|
||||
"description": "Name of the sheet from which to get the data"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"sheet"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Response Get Sheet Contents",
|
||||
"description": "Sheet data as string, rows separated by newlines"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/actions/google-sheet-gmail/send-email-via-gmail/run": {
|
||||
"post": {
|
||||
"summary": "Send Email Via Gmail",
|
||||
"description": "Sends an email using Gmail SMTP with an App Password for authentication.",
|
||||
"operationId": "send_email_via_gmail",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"title": "Subject",
|
||||
"description": "Email subject"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"title": "Body",
|
||||
"description": "Email body content"
|
||||
},
|
||||
"recipient": {
|
||||
"type": "string",
|
||||
"title": "Recipient",
|
||||
"description": "Recipient email address"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"subject",
|
||||
"body",
|
||||
"recipient"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Response Send Email Via Gmail",
|
||||
"description": "Information if the send was successful or not"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
}
|
||||
],
|
||||
"x-openai-isConsequential": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"HTTPBearer": {
|
||||
"type": "http",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"errors": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Errors"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "HTTPValidationError"
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Location"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "Error Type"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"loc",
|
||||
"msg",
|
||||
"type"
|
||||
],
|
||||
"title": "ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,10 @@
|
||||
"""Test toolkit integration."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from langchain_core.utils.function_calling import convert_to_openai_function
|
||||
|
||||
from langchain_robocorp.toolkits import ActionServerToolkit
|
||||
|
||||
from ._fixtures import FakeChatLLMT
|
||||
@ -7,3 +13,108 @@ from ._fixtures import FakeChatLLMT
|
||||
def test_initialization() -> None:
|
||||
"""Test toolkit initialization."""
|
||||
ActionServerToolkit(url="http://localhost", llm=FakeChatLLMT())
|
||||
|
||||
|
||||
def test_get_tools_success() -> None:
|
||||
# Setup
|
||||
toolkit_instance = ActionServerToolkit(
|
||||
url="http://example.com", api_key="dummy_key"
|
||||
)
|
||||
|
||||
fixture_path = Path(__file__).with_name("_openapi2.fixture.json")
|
||||
|
||||
with patch(
|
||||
"langchain_robocorp.toolkits.requests.get"
|
||||
) as mocked_get, fixture_path.open("r") as f:
|
||||
data = json.load(f) # Using json.load directly on the file object
|
||||
mocked_response = MagicMock()
|
||||
mocked_response.json.return_value = data
|
||||
mocked_response.status_code = 200
|
||||
mocked_response.headers = {"Content-Type": "application/json"}
|
||||
mocked_get.return_value = mocked_response
|
||||
|
||||
# Execute
|
||||
tools = toolkit_instance.get_tools()
|
||||
|
||||
# Verify
|
||||
assert len(tools) == 5
|
||||
|
||||
tool = tools[2]
|
||||
assert tool.name == "add_sheet_rows"
|
||||
assert tool.description == (
|
||||
"Action to add multiple rows to the Google sheet. "
|
||||
"Get the sheets with get_google_spreadsheet_schema if you don't know"
|
||||
"\nthe names or data structure. Make sure the values are in correct"
|
||||
""" columns (needs to be ordered the same as in the sample).
|
||||
Strictly adhere to the schema."""
|
||||
)
|
||||
|
||||
openai_func_spec = convert_to_openai_function(tool)
|
||||
|
||||
assert isinstance(
|
||||
openai_func_spec, dict
|
||||
), "openai_func_spec should be a dictionary."
|
||||
assert set(openai_func_spec.keys()) == {
|
||||
"description",
|
||||
"name",
|
||||
"parameters",
|
||||
}, "Top-level keys mismatch."
|
||||
|
||||
assert openai_func_spec["description"] == tool.description
|
||||
assert openai_func_spec["name"] == tool.name
|
||||
|
||||
assert isinstance(
|
||||
openai_func_spec["parameters"], dict
|
||||
), "Parameters should be a dictionary."
|
||||
|
||||
params = openai_func_spec["parameters"]
|
||||
assert set(params.keys()) == {
|
||||
"type",
|
||||
"properties",
|
||||
"required",
|
||||
}, "Parameters keys mismatch."
|
||||
assert params["type"] == "object", "`type` in parameters should be 'object'."
|
||||
assert isinstance(
|
||||
params["properties"], dict
|
||||
), "`properties` should be a dictionary."
|
||||
assert isinstance(params["required"], list), "`required` should be a list."
|
||||
|
||||
assert set(params["required"]) == {
|
||||
"sheet",
|
||||
"rows_to_add",
|
||||
}, "Required fields mismatch."
|
||||
|
||||
assert set(params["properties"].keys()) == {"sheet", "rows_to_add"}
|
||||
|
||||
desc = "The columns that make up the row"
|
||||
expected = {
|
||||
"description": "the rows to be added to the sheet",
|
||||
"allOf": [
|
||||
{
|
||||
"title": "Rows To Add",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rows": {
|
||||
"title": "Rows",
|
||||
"description": "The rows that need to be added",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"title": "Row",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"columns": {
|
||||
"title": "Columns",
|
||||
"description": desc,
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
}
|
||||
},
|
||||
"required": ["columns"],
|
||||
},
|
||||
}
|
||||
},
|
||||
"required": ["rows"],
|
||||
}
|
||||
],
|
||||
}
|
||||
assert params["properties"]["rows_to_add"] == expected
|
||||
|
Loading…
Reference in New Issue
Block a user