diff --git a/libs/partners/robocorp/langchain_robocorp/_common.py b/libs/partners/robocorp/langchain_robocorp/_common.py index 32ce51d44e3..cd5dbd97878 100644 --- a/libs/partners/robocorp/langchain_robocorp/_common.py +++ b/libs/partners/robocorp/langchain_robocorp/_common.py @@ -1,5 +1,6 @@ +import time from dataclasses import dataclass -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Set, Tuple, Union from langchain_core.pydantic_v1 import BaseModel, Field, create_model from langchain_core.utils.json_schema import dereference_refs @@ -93,26 +94,45 @@ def get_schema(endpoint_spec: dict) -> dict: ) -def create_field(schema: dict, required: bool) -> Tuple[Any, Any]: +def create_field( + schema: dict, required: bool, created_model_names: Set[str] +) -> Tuple[Any, Any]: """ Creates a Pydantic field based on the schema definition. """ - field_type = type_mapping.get(schema.get("type", "string"), str) + if "anyOf" in schema: + field_types = [ + create_field(sub_schema, required, created_model_names)[0] + for sub_schema in schema["anyOf"] + ] + if len(field_types) == 1: + field_type = field_types[0] # Simplified handling + else: + field_type = Union[tuple(field_types)] + else: + field_type = type_mapping.get(schema.get("type", "string"), str) + description = schema.get("description", "") # Handle nested objects - if schema["type"] == "object": + if schema.get("type") == "object": nested_fields = { - k: create_field(v, k in schema.get("required", [])) + k: create_field(v, k in schema.get("required", []), created_model_names) for k, v in schema.get("properties", {}).items() } - model_name = schema.get("title", "NestedModel") + model_name = schema.get("title", f"NestedModel{time.time()}") + if model_name in created_model_names: + # needs to be unique + model_name = model_name + str(time.time()) nested_model = create_model(model_name, **nested_fields) # type: ignore + created_model_names.add(model_name) return nested_model, Field(... if required else None, description=description) # Handle arrays - elif schema["type"] == "array": - item_type, _ = create_field(schema["items"], required=True) + elif schema.get("type") == "array": + item_type, _ = create_field( + schema["items"], required=True, created_model_names=created_model_names + ) return List[item_type], Field( # type: ignore ... if required else None, description=description ) @@ -128,9 +148,10 @@ def get_param_fields(endpoint_spec: dict) -> dict: required_fields = schema.get("required", []) fields = {} + created_model_names: Set[str] = set() for key, value in properties.items(): is_required = key in required_fields - field_info = create_field(value, is_required) + field_info = create_field(value, is_required, created_model_names) fields[key] = field_info return fields diff --git a/libs/partners/robocorp/tests/unit_tests/_openapi3.fixture.json b/libs/partners/robocorp/tests/unit_tests/_openapi3.fixture.json new file mode 100644 index 00000000000..97f07b21876 --- /dev/null +++ b/libs/partners/robocorp/tests/unit_tests/_openapi3.fixture.json @@ -0,0 +1,1891 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sema4.ai Action Server", + "version": "0.11.0" + }, + "servers": [ + { + "url": "http://localhost:8806" + } + ], + "paths": { + "/api/actions/google-calendar/create-event/run": { + "post": { + "summary": "Create Event", + "description": "Creates a new event in the specified calendar.", + "operationId": "create_event", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "event": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the event." + }, + "summary": { + "type": "string", + "title": "Summary", + "description": "A short summary of the event's purpose." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Location", + "description": "The physical location of the event." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "A more detailed description of the event." + }, + "start": { + "allOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + } + ], + "description": "The (inclusive) start time of the event." + }, + "end": { + "allOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + } + ], + "description": "The (exclusive) end time of the event." + }, + "recurrence": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Recurrence", + "description": "A list of RRULE, EXRULE, RDATE, and EXDATE lines for a recurring event." + }, + "attendees": { + "anyOf": [ + { + "items": { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "The email address of the identity." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Displayname", + "description": "The display name of the identity." + }, + "optional": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Optional", + "description": "Whether this is an optional attendee." + }, + "responseStatus": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Responsestatus", + "description": "The response status of the attendee." + }, + "organizer": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Organizer", + "description": "Whether the attendee is the organizer of the event." + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "Attendee" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Attendees", + "description": "A list of attendees." + }, + "reminders": { + "anyOf": [ + { + "properties": { + "useDefault": { + "type": "boolean", + "title": "Usedefault", + "description": "Indicates whether to use the default reminders." + }, + "overrides": { + "anyOf": [ + { + "items": { + "properties": { + "method": { + "type": "string", + "title": "Method", + "description": "The method of the reminder (email or popup)." + }, + "minutes": { + "type": "integer", + "title": "Minutes", + "description": "The number of minutes before the event when the reminder should occur." + } + }, + "type": "object", + "required": [ + "method", + "minutes" + ], + "title": "ReminderOverride" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Overrides", + "description": "A list of overrides for the reminders." + } + }, + "type": "object", + "required": [ + "useDefault" + ], + "title": "Reminder" + }, + { + "type": "null" + } + ], + "description": "Reminders settings for the event." + }, + "organizer": { + "allOf": [ + { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "The email address of the identity." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Displayname", + "description": "The display name of the identity." + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "Identity" + } + ], + "description": "The organizer of the event." + } + }, + "type": "object", + "required": [ + "id", + "summary", + "start", + "end", + "organizer" + ], + "title": "Event", + "description": "JSON representation of the Google Calendar V3 event." + }, + "calendar_id": { + "type": "string", + "title": "Calendar Id", + "description": "Calendar identifier which can be found by listing all calendars action.\nDefault value is \"primary\" which indicates the calendar where the user is currently logged in.", + "default": "primary" + } + }, + "type": "object", + "required": [ + "event" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the event." + }, + "summary": { + "type": "string", + "title": "Summary", + "description": "A short summary of the event's purpose." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Location", + "description": "The physical location of the event." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "A more detailed description of the event." + }, + "start": { + "allOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + } + ], + "description": "The (inclusive) start time of the event." + }, + "end": { + "allOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + } + ], + "description": "The (exclusive) end time of the event." + }, + "recurrence": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Recurrence", + "description": "A list of RRULE, EXRULE, RDATE, and EXDATE lines for a recurring event." + }, + "attendees": { + "anyOf": [ + { + "items": { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "The email address of the identity." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Displayname", + "description": "The display name of the identity." + }, + "optional": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Optional", + "description": "Whether this is an optional attendee." + }, + "responseStatus": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Responsestatus", + "description": "The response status of the attendee." + }, + "organizer": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Organizer", + "description": "Whether the attendee is the organizer of the event." + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "Attendee" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Attendees", + "description": "A list of attendees." + }, + "reminders": { + "anyOf": [ + { + "properties": { + "useDefault": { + "type": "boolean", + "title": "Usedefault", + "description": "Indicates whether to use the default reminders." + }, + "overrides": { + "anyOf": [ + { + "items": { + "properties": { + "method": { + "type": "string", + "title": "Method", + "description": "The method of the reminder (email or popup)." + }, + "minutes": { + "type": "integer", + "title": "Minutes", + "description": "The number of minutes before the event when the reminder should occur." + } + }, + "type": "object", + "required": [ + "method", + "minutes" + ], + "title": "ReminderOverride" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Overrides", + "description": "A list of overrides for the reminders." + } + }, + "type": "object", + "required": [ + "useDefault" + ], + "title": "Reminder" + }, + { + "type": "null" + } + ], + "description": "Reminders settings for the event." + }, + "organizer": { + "allOf": [ + { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "The email address of the identity." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Displayname", + "description": "The display name of the identity." + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "Identity" + } + ], + "description": "The organizer of the event." + } + }, + "type": "object", + "required": [ + "id", + "summary", + "start", + "end", + "organizer" + ], + "title": "Response for Create Event", + "description": "The newly created event." + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-openai-isConsequential": true + } + }, + "/api/actions/google-calendar/list-events/run": { + "post": { + "summary": "List Events", + "description": "List all events in the user's primary calendar between the given dates.\nTo aggregate all events across calendars, call this method for each calendar returned by list_calendars endpoint.", + "operationId": "list_events", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "calendar_id": { + "type": "string", + "title": "Calendar Id", + "description": "Calendar identifier which can be found by listing all calendars action.\nDefault value is \"primary\" which indicates the calendar where the user is currently logged in.", + "default": "primary" + }, + "query": { + "type": "string", + "title": "Query", + "description": "Free text search terms to find events that match these terms in summary, description, location,\nattendee's name / email or working location information.", + "default": "" + }, + "start_date": { + "type": "string", + "title": "Start Date", + "description": "Upper bound (exclusive) for an event's start time to filter by.\nMust be an RFC3339 timestamp with mandatory time zone offset.", + "default": "" + }, + "end_date": { + "type": "string", + "title": "End Date", + "description": "Lower bound (exclusive) for an event's end time to filter by.\nMust be an RFC3339 timestamp with mandatory time zone offset.", + "default": "" + } + }, + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "anyOf": [ + { + "properties": { + "events": { + "items": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the event." + }, + "summary": { + "type": "string", + "title": "Summary", + "description": "A short summary of the event's purpose." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Location", + "description": "The physical location of the event." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "A more detailed description of the event." + }, + "start": { + "allOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + } + ], + "description": "The (inclusive) start time of the event." + }, + "end": { + "allOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + } + ], + "description": "The (exclusive) end time of the event." + }, + "recurrence": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Recurrence", + "description": "A list of RRULE, EXRULE, RDATE, and EXDATE lines for a recurring event." + }, + "attendees": { + "anyOf": [ + { + "items": { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "The email address of the identity." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Displayname", + "description": "The display name of the identity." + }, + "optional": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Optional", + "description": "Whether this is an optional attendee." + }, + "responseStatus": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Responsestatus", + "description": "The response status of the attendee." + }, + "organizer": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Organizer", + "description": "Whether the attendee is the organizer of the event." + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "Attendee" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Attendees", + "description": "A list of attendees." + }, + "reminders": { + "anyOf": [ + { + "properties": { + "useDefault": { + "type": "boolean", + "title": "Usedefault", + "description": "Indicates whether to use the default reminders." + }, + "overrides": { + "anyOf": [ + { + "items": { + "properties": { + "method": { + "type": "string", + "title": "Method", + "description": "The method of the reminder (email or popup)." + }, + "minutes": { + "type": "integer", + "title": "Minutes", + "description": "The number of minutes before the event when the reminder should occur." + } + }, + "type": "object", + "required": [ + "method", + "minutes" + ], + "title": "ReminderOverride" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Overrides", + "description": "A list of overrides for the reminders." + } + }, + "type": "object", + "required": [ + "useDefault" + ], + "title": "Reminder" + }, + { + "type": "null" + } + ], + "description": "Reminders settings for the event." + }, + "organizer": { + "allOf": [ + { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "The email address of the identity." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Displayname", + "description": "The display name of the identity." + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "Identity" + } + ], + "description": "The organizer of the event." + } + }, + "type": "object", + "required": [ + "id", + "summary", + "start", + "end", + "organizer" + ], + "title": "Event" + }, + "type": "array", + "title": "Events" + } + }, + "type": "object", + "required": [ + "events" + ], + "title": "EventList" + }, + { + "type": "null" + } + ], + "description": "The result for the action if it ran successfully" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error", + "description": "The error message if the action failed for some reason" + } + }, + "type": "object", + "title": "Response for List Events", + "description": "A list of calendar events that match the query, if defined." + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-openai-isConsequential": false + } + }, + "/api/actions/google-calendar/list-calendars/run": { + "post": { + "summary": "List Calendars", + "description": "List all calendars that the user is subscribed to.", + "operationId": "list_calendars", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "anyOf": [ + { + "properties": { + "calendars": { + "items": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the calendar." + }, + "summary": { + "type": "string", + "title": "Summary", + "description": "The name or summary of the calendar." + }, + "timeZone": { + "type": "string", + "title": "Timezone", + "description": "The timezone the calendar is set to, such as 'Europe/Bucharest'." + }, + "selected": { + "type": "boolean", + "title": "Selected", + "description": "A boolean indicating if the calendar is selected by the user in their UI." + }, + "accessRole": { + "type": "string", + "title": "Accessrole", + "description": "The access role of the user with respect to the calendar, e.g., 'owner'." + } + }, + "type": "object", + "required": [ + "id", + "summary", + "timeZone", + "selected", + "accessRole" + ], + "title": "Calendar" + }, + "type": "array", + "title": "Calendars" + } + }, + "type": "object", + "required": [ + "calendars" + ], + "title": "CalendarList" + }, + { + "type": "null" + } + ], + "description": "The result for the action if it ran successfully" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error", + "description": "The error message if the action failed for some reason" + } + }, + "type": "object", + "title": "Response for List Calendars", + "description": "A list of calendars." + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-openai-isConsequential": false + } + }, + "/api/actions/google-calendar/update-event/run": { + "post": { + "summary": "Update Event", + "description": "Update an existing Google Calendar event with dynamic arguments.", + "operationId": "update_event", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "event_id": { + "type": "string", + "title": "Event Id", + "description": "Identifier of the event to update. Can be found by listing events in all calendars." + }, + "updates": { + "properties": { + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Summary", + "description": "A short summary of the event's purpose." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Location", + "description": "The physical location of the event." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "A more detailed description of the event." + }, + "start": { + "anyOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + }, + { + "type": "null" + } + ], + "description": "The (inclusive) start time of the event." + }, + "end": { + "anyOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + }, + { + "type": "null" + } + ], + "description": "The (exclusive) end time of the event." + }, + "attendees": { + "anyOf": [ + { + "items": { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "The email address of the identity." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Displayname", + "description": "The display name of the identity." + }, + "optional": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Optional", + "description": "Whether this is an optional attendee." + }, + "responseStatus": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Responsestatus", + "description": "The response status of the attendee." + }, + "organizer": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Organizer", + "description": "Whether the attendee is the organizer of the event." + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "Attendee" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Attendees", + "description": "A list of attendees consisting in email and whether they are mandatory to participate or not." + } + }, + "type": "object", + "title": "Updates", + "description": "A dictionary containing the event attributes to update.\nPossible keys include 'summary', 'description', 'start', 'end', and 'attendees'." + }, + "calendar_id": { + "type": "string", + "title": "Calendar Id", + "description": "Identifier of the calendar where the event is.\nDefault value is \"primary\" which indicates the calendar where the user is currently logged in.", + "default": "primary" + } + }, + "type": "object", + "required": [ + "event_id", + "updates" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "properties": { + "result": { + "anyOf": [ + { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the event." + }, + "summary": { + "type": "string", + "title": "Summary", + "description": "A short summary of the event's purpose." + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Location", + "description": "The physical location of the event." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "A more detailed description of the event." + }, + "start": { + "allOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + } + ], + "description": "The (inclusive) start time of the event." + }, + "end": { + "allOf": [ + { + "properties": { + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date", + "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event." + }, + "dateTime": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datetime", + "description": "The start or end time of the event." + }, + "timeZone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional." + } + }, + "type": "object", + "title": "EventDateTime" + } + ], + "description": "The (exclusive) end time of the event." + }, + "recurrence": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Recurrence", + "description": "A list of RRULE, EXRULE, RDATE, and EXDATE lines for a recurring event." + }, + "attendees": { + "anyOf": [ + { + "items": { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "The email address of the identity." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Displayname", + "description": "The display name of the identity." + }, + "optional": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Optional", + "description": "Whether this is an optional attendee." + }, + "responseStatus": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Responsestatus", + "description": "The response status of the attendee." + }, + "organizer": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Organizer", + "description": "Whether the attendee is the organizer of the event." + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "Attendee" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Attendees", + "description": "A list of attendees." + }, + "reminders": { + "anyOf": [ + { + "properties": { + "useDefault": { + "type": "boolean", + "title": "Usedefault", + "description": "Indicates whether to use the default reminders." + }, + "overrides": { + "anyOf": [ + { + "items": { + "properties": { + "method": { + "type": "string", + "title": "Method", + "description": "The method of the reminder (email or popup)." + }, + "minutes": { + "type": "integer", + "title": "Minutes", + "description": "The number of minutes before the event when the reminder should occur." + } + }, + "type": "object", + "required": [ + "method", + "minutes" + ], + "title": "ReminderOverride" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Overrides", + "description": "A list of overrides for the reminders." + } + }, + "type": "object", + "required": [ + "useDefault" + ], + "title": "Reminder" + }, + { + "type": "null" + } + ], + "description": "Reminders settings for the event." + }, + "organizer": { + "allOf": [ + { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "The email address of the identity." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Displayname", + "description": "The display name of the identity." + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "Identity" + } + ], + "description": "The organizer of the event." + } + }, + "type": "object", + "required": [ + "id", + "summary", + "start", + "end", + "organizer" + ], + "title": "Event" + }, + { + "type": "null" + } + ], + "description": "The result for the action if it ran successfully" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error", + "description": "The error message if the action failed for some reason" + } + }, + "type": "object", + "title": "Response for Update Event", + "description": "Updated event details." + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-openai-isConsequential": true + } + } + }, + "components": { + "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" + } + } + } +} diff --git a/libs/partners/robocorp/tests/unit_tests/test_toolkits.py b/libs/partners/robocorp/tests/unit_tests/test_toolkits.py index 47b410eba05..0b3a0e14e3b 100644 --- a/libs/partners/robocorp/tests/unit_tests/test_toolkits.py +++ b/libs/partners/robocorp/tests/unit_tests/test_toolkits.py @@ -3,7 +3,10 @@ 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_core.utils.function_calling import ( + convert_to_openai_function, + convert_to_openai_tool, +) from langchain_robocorp.toolkits import ActionServerToolkit @@ -118,3 +121,66 @@ Strictly adhere to the schema.""" ], } assert params["properties"]["rows_to_add"] == expected + + +def test_get_tools_with_complex_inputs() -> None: + toolkit_instance = ActionServerToolkit( + url="http://example.com", api_key="dummy_key" + ) + + fixture_path = Path(__file__).with_name("_openapi3.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() + assert len(tools) == 4 + + tool = tools[0] + assert tool.name == "create_event" + assert tool.description == "Creates a new event in the specified calendar." + + all_tools_as_openai_tools = [convert_to_openai_tool(t) for t in tools] + openai_tool_spec = all_tools_as_openai_tools[0]["function"] + + assert isinstance( + openai_tool_spec, dict + ), "openai_func_spec should be a dictionary." + assert set(openai_tool_spec.keys()) == { + "description", + "name", + "parameters", + }, "Top-level keys mismatch." + + assert openai_tool_spec["description"] == tool.description + assert openai_tool_spec["name"] == tool.name + + assert isinstance( + openai_tool_spec["parameters"], dict + ), "Parameters should be a dictionary." + + params = openai_tool_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"]) == { + "event", + }, "Required fields mismatch." + + assert set(params["properties"].keys()) == {"calendar_id", "event"}