Skip to content

Commit

Permalink
robocorp[patch]: Fix nested arguments descriptors and tool names (#19707
Browse files Browse the repository at this point in the history
)

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.
  • Loading branch information
mkorpela committed Apr 1, 2024
1 parent 48f84e2 commit 3f06cef
Show file tree
Hide file tree
Showing 8 changed files with 580 additions and 70 deletions.
3 changes: 2 additions & 1 deletion libs/partners/robocorp/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
88 changes: 54 additions & 34 deletions libs/partners/robocorp/langchain_robocorp/_common.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -72,28 +73,6 @@ def reduce_endpoint_docs(docs: dict) -> dict:
)


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,
Expand All @@ -105,25 +84,66 @@ def get_required_param_descriptions(endpoint_spec: dict) -> str:
}


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
12 changes: 6 additions & 6 deletions libs/partners/robocorp/langchain_robocorp/_prompts.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -16,3 +15,4 @@
User Input: {input}
"""
)
28 changes: 10 additions & 18 deletions libs/partners/robocorp/langchain_robocorp/toolkits.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@

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
Expand Down Expand Up @@ -156,17 +155,9 @@ def get_tools(
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,
}

Expand Down Expand Up @@ -218,16 +209,17 @@ def _get_structured_tool(
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,
)

Expand Down
17 changes: 8 additions & 9 deletions libs/partners/robocorp/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions libs/partners/robocorp/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down

0 comments on commit 3f06cef

Please sign in to comment.