From 76020e70adbb4115310645dcb7c175b700147753 Mon Sep 17 00:00:00 2001 From: codebanesr Date: Sun, 24 Sep 2023 23:40:24 +0300 Subject: [PATCH 1/8] Adding plan and executor chain --- llm-server/app.py | 2 +- llm-server/docs/petstore_fixed.json | 1225 +++++++++++++++++ .../routes/workflow/hierarchical_planner.py | 27 + .../routes/workflow/workflow_service.py | 5 +- 4 files changed, 1256 insertions(+), 3 deletions(-) create mode 100644 llm-server/docs/petstore_fixed.json create mode 100644 llm-server/routes/workflow/hierarchical_planner.py diff --git a/llm-server/app.py b/llm-server/app.py index 95849127b..bdc6036ba 100644 --- a/llm-server/app.py +++ b/llm-server/app.py @@ -47,7 +47,7 @@ def handle(): WorkflowData(text, swagger_url, headers, server_base_url) ) except Exception as e: - print(f"Using agent: {e}") + raise e if swagger_url.startswith("https://"): full_url = swagger_url diff --git a/llm-server/docs/petstore_fixed.json b/llm-server/docs/petstore_fixed.json new file mode 100644 index 000000000..c2c870519 --- /dev/null +++ b/llm-server/docs/petstore_fixed.json @@ -0,0 +1,1225 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Swagger Petstore - OpenAPI 3.0", + "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.17" + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + }, + "servers": [ + { + "url": "https://petstore3.swagger.io/api/v3" + } + ], + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + }, + { + "name": "user", + "description": "Operations about user" + } + ], + "paths": { + "/pet": { + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "description": "Update an existent pet in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "description": "Create a new pet in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "explode": true, + "schema": { + "type": "string", + "default": "available", + "enum": [ + "available", + "pending", + "sold" + ] + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "api_key": [] + }, + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "name", + "in": "query", + "description": "Name of pet that needs to be updated", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Status of pet that needs to be updated", + "schema": { + "type": "string" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "parameters": [ + { + "name": "api_key", + "in": "header", + "description": "", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid pet value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "additionalMetadata", + "in": "query", + "description": "Additional Metadata", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "Place a new order in the store", + "operationId": "placeOrder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "405": { + "description": "Invalid input" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", + "operationId": "getOrderById", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of order that needs to be fetched", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "operationId": "deleteOrder", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "description": "Created user object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "default": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "Creates list of users with given input array", + "operationId": "createUsersWithListInput", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "headers": { + "X-Rate-Limit": { + "description": "calls per hour allowed by the user", + "schema": { + "type": "integer", + "format": "int32" + } + }, + "X-Expires-After": { + "description": "date in UTC when token expires", + "schema": { + "type": "string", + "format": "date-time" + } + } + }, + "content": { + "application/xml": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "parameters": [], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be deleted", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Update an existent user in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "default": { + "description": "successful operation" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "components": { + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "petId": { + "type": "integer", + "format": "int64", + "example": 198772 + }, + "quantity": { + "type": "integer", + "format": "int32", + "example": 7 + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "example": "approved", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean" + } + }, + "xml": { + "name": "order" + } + }, + "Customer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 100000 + }, + "username": { + "type": "string", + "example": "fehguy" + }, + "address": { + "type": "array", + "xml": { + "name": "addresses", + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Address" + } + } + }, + "xml": { + "name": "customer" + } + }, + "Address": { + "type": "object", + "properties": { + "street": { + "type": "string", + "example": "437 Lytton" + }, + "city": { + "type": "string", + "example": "Palo Alto" + }, + "state": { + "type": "string", + "example": "CA" + }, + "zip": { + "type": "string", + "example": "94301" + } + }, + "xml": { + "name": "address" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "type": "string", + "example": "Dogs" + } + }, + "xml": { + "name": "category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "username": { + "type": "string", + "example": "theUser" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "lastName": { + "type": "string", + "example": "James" + }, + "email": { + "type": "string", + "example": "john@email.com" + }, + "password": { + "type": "string", + "example": "12345" + }, + "phone": { + "type": "string", + "example": "12345" + }, + "userStatus": { + "type": "integer", + "description": "User Status", + "format": "int32", + "example": 1 + } + }, + "xml": { + "name": "user" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "tag" + } + }, + "Pet": { + "required": [ + "name", + "photoUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "name": { + "type": "string", + "example": "doggie" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "photoUrls": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "type": "string", + "xml": { + "name": "photoUrl" + } + } + }, + "tags": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "xml": { + "name": "##default" + } + } + }, + "requestBodies": { + "Pet": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "UserArray": { + "description": "List of user object", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + } + } +} \ No newline at end of file diff --git a/llm-server/routes/workflow/hierarchical_planner.py b/llm-server/routes/workflow/hierarchical_planner.py new file mode 100644 index 000000000..16b4a1ac1 --- /dev/null +++ b/llm-server/routes/workflow/hierarchical_planner.py @@ -0,0 +1,27 @@ +from typing import Dict, Any + +from langchain.agents.agent_toolkits.openapi.spec import reduce_openapi_spec +from langchain.requests import RequestsWrapper +from langchain.llms.openai import OpenAI +from langchain.agents.agent_toolkits.openapi import planner +from routes.workflow.load_openapi_spec import load_openapi_spec + + +def create_and_run_openapi_agent(spec_path: str, user_query: str, api_key: str) -> Any: + # Load OpenAPI spec + raw_spec = load_openapi_spec(spec_path) + spec = reduce_openapi_spec(raw_spec) + + # Create RequestsWrapper with auth + headers: Dict[str, str] = {"Authorization": f"Bearer {api_key}"} + requests_wrapper: RequestsWrapper = RequestsWrapper(headers=headers) + + # Create OpenAPI agent + llm: OpenAI = OpenAI(model_name="gpt-4", temperature=0.0) + agent: Any = planner.create_openapi_agent(spec, requests_wrapper, llm) + + # Run agent on user query + response = agent.run(user_query) + + print(response) + return response diff --git a/llm-server/routes/workflow/workflow_service.py b/llm-server/routes/workflow/workflow_service.py index f7f73b71d..ca502e1b5 100644 --- a/llm-server/routes/workflow/workflow_service.py +++ b/llm-server/routes/workflow/workflow_service.py @@ -10,6 +10,7 @@ from routes.workflow.typings.run_workflow_input import WorkflowData from langchain.tools.json.tool import JsonSpec from typing import List +from routes.workflow.hierarchical_planner import create_and_run_openapi_agent import json from typing import Any, Dict, Optional, cast, Union @@ -52,7 +53,7 @@ def run_workflow(data: WorkflowData) -> Any: (document, score) = vector_store.similarity_search_with_relevance_scores(text)[0] - if score > 0.9: + if score > 0.95: print( f"Record '{document}' is highly similar with a similarity score of {document}" ) @@ -67,7 +68,7 @@ def run_workflow(data: WorkflowData) -> Any: return result, 200, {"Content-Type": "application/json"} else: # call openapi spec - raise Exception("Workflow not defined for this request, try using an agent") + create_and_run_openapi_agent(swagger_src, text, "API_KEY") def run_openapi_operations( From 4201a70f899f3a1cb334ba491e32fd3cc1bbd600 Mon Sep 17 00:00:00 2001 From: codebanesr Date: Mon, 25 Sep 2023 01:08:01 +0300 Subject: [PATCH 2/8] Updating Query planner response --- llm-server/app.py | 4 ++- llm-server/requirements.txt | 30 ++++++++++++++++--- .../routes/workflow/hierarchical_planner.py | 9 +++--- .../workflow/typings/run_workflow_input.py | 6 ++-- .../routes/workflow/workflow_service.py | 7 +++-- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/llm-server/app.py b/llm-server/app.py index bdc6036ba..7dfe0771a 100644 --- a/llm-server/app.py +++ b/llm-server/app.py @@ -43,9 +43,11 @@ def handle(): try: if not hasSingleIntent(swagger_url, text): - return run_workflow( + result = run_workflow( WorkflowData(text, swagger_url, headers, server_base_url) ) + + return result except Exception as e: raise e diff --git a/llm-server/requirements.txt b/llm-server/requirements.txt index 36539297c..e176c849b 100644 --- a/llm-server/requirements.txt +++ b/llm-server/requirements.txt @@ -1,6 +1,6 @@ aiohttp==3.8.5 aiosignal==1.3.1 -anyio==4.0.0 +anyio==3.7.1 asgiref==3.7.2 async-timeout==4.0.2 attrs==23.1.0 @@ -9,11 +9,13 @@ blinker==1.6.2 blis==0.7.9 catalogue==2.0.8 certifi==2023.7.22 +chardet==5.2.0 charset-normalizer==3.2.0 click==8.1.5 confection==0.1.0 cymem==2.0.7 dataclasses-json==0.5.9 +deptry==0.12.0 dill==0.3.6 dnspython==2.4.2 elastic-transport==8.4.0 @@ -39,35 +41,51 @@ huggingface-hub==0.16.4 hyperframe==6.0.1 idna==3.4 importlib-metadata==6.8.0 +isodate==0.6.1 itsdangerous==2.1.2 Jinja2==3.1.2 joblib==1.3.1 +jsonpatch==1.33 +jsonpointer==2.4 jsonschema==4.19.0 +jsonschema-spec==0.2.4 jsonschema-specifications==2023.7.1 -langchain==0.0.232 +langchain==0.0.300 langcodes==3.3.0 -litellm>=0.1.574 -langsmith==0.0.5 +langsmith==0.0.40 +lazy-object-proxy==1.9.0 +litellm==0.1.729 loguru==0.7.1 manifest-ml==0.0.1 MarkupSafe==2.1.3 marshmallow==3.19.0 marshmallow-enum==1.5.1 +mccabe @ file:///opt/conda/conda-bld/mccabe_1644221741721/work +more-itertools==10.1.0 multidict==6.0.4 murmurhash==1.0.9 +mypy @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_b7yrbsawof/croot/mypy_1668440220235/work mypy-extensions==0.4.3 nltk==3.8.1 numexpr==2.8.4 numpy==1.25.1 openai==0.27.8 openapi-schema-pydantic==1.2.4 +openapi-schema-validator==0.6.0 +openapi-spec-validator==0.6.0 packaging==23.1 +parse==1.19.1 +pathable==0.4.3 +pathspec==0.11.2 pathy==0.10.2 pinecone-client==2.2.2 portalocker==2.7.0 preshed==3.0.8 protobuf==4.24.2 +psutil @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_1310b568-21f4-4cb0-b0e3-2f3d31e39728k9coaga5/croots/recipe/psutil_1656431280844/work +pycodestyle @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_5b2mq44vl0/croot/pycodestyle_1674267228581/work pydantic==1.10.11 +pyflakes @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_d9_v8e0_nr/croot/pyflakes_1674165137080/work pymongo==4.5.0 python-dateutil==2.8.2 python-dotenv==1.0.0 @@ -77,6 +95,7 @@ redis==4.6.0 referencing==0.30.2 regex==2023.6.3 requests==2.31.0 +rfc3339-validator==0.1.4 rpds-py==0.10.2 safetensors==0.3.1 six==1.16.0 @@ -92,6 +111,8 @@ tenacity==8.2.2 thinc==8.1.10 tiktoken==0.4.0 tokenizers==0.13.3 +toml @ file:///tmp/build/80754af9/toml_1616166611790/work +tomli @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_d0e5ffbf-5cf1-45be-8693-c5dff8108a2awhthtjlq/croots/recipe/tomli_1657175508477/work tqdm==4.65.0 transformers==4.30.2 typer==0.9.0 @@ -99,6 +120,7 @@ types-PyYAML==6.0.12.11 types-requests==2.31.0.2 types-urllib3==1.26.25.14 typing-inspect==0.9.0 +typing_extensions @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_1fdywrbp_3/croot/typing_extensions_1690297474455/work urllib3==1.26.16 wasabi==1.1.2 Werkzeug==2.3.6 diff --git a/llm-server/routes/workflow/hierarchical_planner.py b/llm-server/routes/workflow/hierarchical_planner.py index 16b4a1ac1..ff9eee448 100644 --- a/llm-server/routes/workflow/hierarchical_planner.py +++ b/llm-server/routes/workflow/hierarchical_planner.py @@ -7,21 +7,20 @@ from routes.workflow.load_openapi_spec import load_openapi_spec -def create_and_run_openapi_agent(spec_path: str, user_query: str, api_key: str) -> Any: +def create_and_run_openapi_agent( + spec_path: str, user_query: str, headers: Dict[str, str] = {} +) -> Any: # Load OpenAPI spec raw_spec = load_openapi_spec(spec_path) spec = reduce_openapi_spec(raw_spec) # Create RequestsWrapper with auth - headers: Dict[str, str] = {"Authorization": f"Bearer {api_key}"} requests_wrapper: RequestsWrapper = RequestsWrapper(headers=headers) # Create OpenAPI agent llm: OpenAI = OpenAI(model_name="gpt-4", temperature=0.0) - agent: Any = planner.create_openapi_agent(spec, requests_wrapper, llm) + agent = planner.create_openapi_agent(spec, requests_wrapper, llm) # Run agent on user query response = agent.run(user_query) - - print(response) return response diff --git a/llm-server/routes/workflow/typings/run_workflow_input.py b/llm-server/routes/workflow/typings/run_workflow_input.py index c611479be..9c2ed50f7 100644 --- a/llm-server/routes/workflow/typings/run_workflow_input.py +++ b/llm-server/routes/workflow/typings/run_workflow_input.py @@ -1,11 +1,9 @@ -class Headers: - def __init__(self) -> None: - self.data: dict[str, str] = {} +from typing import Dict class WorkflowData: def __init__( - self, text: str, swagger_url: str, headers: Headers, server_base_url: str + self, text: str, swagger_url: str, headers: Dict[str, str], server_base_url: str ) -> None: self.text = text self.swagger_url = swagger_url diff --git a/llm-server/routes/workflow/workflow_service.py b/llm-server/routes/workflow/workflow_service.py index ca502e1b5..7af302ff6 100644 --- a/llm-server/routes/workflow/workflow_service.py +++ b/llm-server/routes/workflow/workflow_service.py @@ -40,7 +40,7 @@ def get_valid_url( def run_workflow(data: WorkflowData) -> Any: text = data.text swagger_src = data.swagger_url - headers = data.headers + headers = data.headers or {} # This will come from request payload later on when implementing multi-tenancy namespace = "workflows" server_base_url = data.server_base_url @@ -65,10 +65,11 @@ def run_workflow(data: WorkflowData) -> Any: result = run_openapi_operations( record, swagger_src, text, headers, server_base_url ) - return result, 200, {"Content-Type": "application/json"} + return result else: # call openapi spec - create_and_run_openapi_agent(swagger_src, text, "API_KEY") + result = create_and_run_openapi_agent(swagger_src, text, headers) + return result def run_openapi_operations( From 5d5979ba8b02e7099de9657dfe75a6cbb9683326 Mon Sep 17 00:00:00 2001 From: codebanesr Date: Mon, 25 Sep 2023 19:49:37 +0300 Subject: [PATCH 3/8] Finishing up hierarchical planner --- llm-server/routes/workflow/hierarchical_planner.py | 2 +- llm-server/routes/workflow/workflow_service.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/llm-server/routes/workflow/hierarchical_planner.py b/llm-server/routes/workflow/hierarchical_planner.py index ff9eee448..f81bf2350 100644 --- a/llm-server/routes/workflow/hierarchical_planner.py +++ b/llm-server/routes/workflow/hierarchical_planner.py @@ -1,10 +1,10 @@ from typing import Dict, Any from langchain.agents.agent_toolkits.openapi.spec import reduce_openapi_spec +from routes.workflow.load_openapi_spec import load_openapi_spec from langchain.requests import RequestsWrapper from langchain.llms.openai import OpenAI from langchain.agents.agent_toolkits.openapi import planner -from routes.workflow.load_openapi_spec import load_openapi_spec def create_and_run_openapi_agent( diff --git a/llm-server/routes/workflow/workflow_service.py b/llm-server/routes/workflow/workflow_service.py index 7af302ff6..35f6c43d5 100644 --- a/llm-server/routes/workflow/workflow_service.py +++ b/llm-server/routes/workflow/workflow_service.py @@ -18,6 +18,10 @@ db_instance = Database() mongo = db_instance.get_db() +import os + +VECTOR_DB_THRESHOLD = float(os.getenv("VECTOR_DB_THRESHOLD", 0.88)) + def get_valid_url( api_payload: Dict[str, Union[str, None]], server_base_url: Optional[str] @@ -53,7 +57,7 @@ def run_workflow(data: WorkflowData) -> Any: (document, score) = vector_store.similarity_search_with_relevance_scores(text)[0] - if score > 0.95: + if score > VECTOR_DB_THRESHOLD: print( f"Record '{document}' is highly similar with a similarity score of {document}" ) From 46243fc321067603c88e2c84ccd450bda3569b46 Mon Sep 17 00:00:00 2001 From: codebanesr Date: Wed, 27 Sep 2023 01:06:00 +0300 Subject: [PATCH 4/8] Fixing issues with planner --- llm-server/.env.example | 7 +- llm-server/api_caller/planner.py | 363 ++++++++++++++++++ llm-server/app.py | 2 +- .../routes/workflow/hierarchical_planner.py | 14 +- .../routes/workflow/workflow_service.py | 45 ++- .../utils/vector_db/get_vector_store.py | 1 + 6 files changed, 408 insertions(+), 24 deletions(-) create mode 100644 llm-server/api_caller/planner.py diff --git a/llm-server/.env.example b/llm-server/.env.example index 41c9a9aa1..6d7816bcb 100644 --- a/llm-server/.env.example +++ b/llm-server/.env.example @@ -12,4 +12,9 @@ STORE=QDRANT LANGCHAIN_TRACING_V2=true LANGCHAIN_ENDPOINT="https://api.smith.langchain.com" LANGCHAIN_API_KEY="TOKEN_GOES_HERE" -LANGCHAIN_PROJECT="PROJECT_NAME_GOES_HERE" \ No newline at end of file +LANGCHAIN_PROJECT="PROJECT_NAME_GOES_HERE" + + +# use gpt-4 for more complicated tasks, but it is usually much slower than 3.5-turbo family +# gpt-3.5-turbo-16k - this model is many times faster than gpt-4, gpt-4 is more accurate with self observation +PLAN_AND_EXECUTE_MODEL=gpt-3.5-turbo-16k \ No newline at end of file diff --git a/llm-server/api_caller/planner.py b/llm-server/api_caller/planner.py new file mode 100644 index 000000000..d218265b2 --- /dev/null +++ b/llm-server/api_caller/planner.py @@ -0,0 +1,363 @@ +# Till this is merged / fixed we will be using our custom planner + +"""Agent that interacts with OpenAPI APIs via a hierarchical planning approach.""" +import json +import re +from functools import partial +from typing import Any, Callable, Dict, List, Optional + +import yaml + +from langchain.agents.agent import AgentExecutor +from langchain.agents.agent_toolkits.openapi.planner_prompt import ( + API_CONTROLLER_PROMPT, + API_CONTROLLER_TOOL_DESCRIPTION, + API_CONTROLLER_TOOL_NAME, + API_ORCHESTRATOR_PROMPT, + API_PLANNER_PROMPT, + API_PLANNER_TOOL_DESCRIPTION, + API_PLANNER_TOOL_NAME, + PARSING_DELETE_PROMPT, + PARSING_GET_PROMPT, + PARSING_PATCH_PROMPT, + PARSING_POST_PROMPT, + PARSING_PUT_PROMPT, + REQUESTS_DELETE_TOOL_DESCRIPTION, + REQUESTS_GET_TOOL_DESCRIPTION, + REQUESTS_PATCH_TOOL_DESCRIPTION, + REQUESTS_POST_TOOL_DESCRIPTION, + REQUESTS_PUT_TOOL_DESCRIPTION, +) +from langchain.agents.agent_toolkits.openapi.spec import ReducedOpenAPISpec +from langchain.agents.mrkl.base import ZeroShotAgent +from langchain.agents.tools import Tool +from langchain.callbacks.base import BaseCallbackManager +from langchain.chains.llm import LLMChain +from langchain.llms.openai import OpenAI +from langchain.memory import ReadOnlySharedMemory +from langchain.prompts import PromptTemplate +from langchain.pydantic_v1 import Field +from langchain.schema import BasePromptTemplate +from langchain.schema.language_model import BaseLanguageModel +from langchain.tools.base import BaseTool +from langchain.tools.requests.tool import BaseRequestsTool +from langchain.utilities.requests import RequestsWrapper + +# +# Requests tools with LLM-instructed extraction of truncated responses. +# +# Of course, truncating so bluntly may lose a lot of valuable +# information in the response. +# However, the goal for now is to have only a single inference step. +MAX_RESPONSE_LENGTH = 5000 +"""Maximum length of the response to be returned.""" + + +def _get_default_llm_chain(prompt: BasePromptTemplate) -> LLMChain: + return LLMChain( + llm=OpenAI(), + prompt=prompt, + ) + + +def _get_default_llm_chain_factory( + prompt: BasePromptTemplate, +) -> Callable[[], LLMChain]: + """Returns a default LLMChain factory.""" + return partial(_get_default_llm_chain, prompt) + + +class RequestsGetToolWithParsing(BaseRequestsTool, BaseTool): + """Requests GET tool with LLM-instructed extraction of truncated responses.""" + + name: str = "requests_get" + """Tool name.""" + description = REQUESTS_GET_TOOL_DESCRIPTION + """Tool description.""" + response_length: Optional[int] = MAX_RESPONSE_LENGTH + """Maximum length of the response to be returned.""" + llm_chain: LLMChain = Field( + default_factory=_get_default_llm_chain_factory(PARSING_GET_PROMPT) + ) + """LLMChain used to extract the response.""" + + def _run(self, text: str) -> str: + try: + data = json.loads(text) + except json.JSONDecodeError as e: + raise e + data_params = data.get("params") + response = self.requests_wrapper.get(data["url"], params=data_params) + response = response[: self.response_length] + return self.llm_chain.predict( + response=response, instructions=data["output_instructions"] + ).strip() + + async def _arun(self, text: str) -> str: + raise NotImplementedError() + + +class RequestsPostToolWithParsing(BaseRequestsTool, BaseTool): + """Requests POST tool with LLM-instructed extraction of truncated responses.""" + + name: str = "requests_post" + """Tool name.""" + description = REQUESTS_POST_TOOL_DESCRIPTION + """Tool description.""" + response_length: Optional[int] = MAX_RESPONSE_LENGTH + """Maximum length of the response to be returned.""" + llm_chain: LLMChain = Field( + default_factory=_get_default_llm_chain_factory(PARSING_POST_PROMPT) + ) + """LLMChain used to extract the response.""" + + def _run(self, text: str) -> str: + try: + data = json.loads(text) + except json.JSONDecodeError as e: + raise e + response = self.requests_wrapper.post(data["url"], data["data"]) + response = response[: self.response_length] + return self.llm_chain.predict( + response=response, instructions=data["output_instructions"] + ).strip() + + async def _arun(self, text: str) -> str: + raise NotImplementedError() + + +class RequestsPatchToolWithParsing(BaseRequestsTool, BaseTool): + """Requests PATCH tool with LLM-instructed extraction of truncated responses.""" + + name: str = "requests_patch" + """Tool name.""" + description = REQUESTS_PATCH_TOOL_DESCRIPTION + """Tool description.""" + response_length: Optional[int] = MAX_RESPONSE_LENGTH + """Maximum length of the response to be returned.""" + llm_chain: LLMChain = Field( + default_factory=_get_default_llm_chain_factory(PARSING_PATCH_PROMPT) + ) + """LLMChain used to extract the response.""" + + def _run(self, text: str) -> str: + try: + data = json.loads(text) + except json.JSONDecodeError as e: + raise e + response = self.requests_wrapper.patch(data["url"], data["data"]) + response = response[: self.response_length] + return self.llm_chain.predict( + response=response, instructions=data["output_instructions"] + ).strip() + + async def _arun(self, text: str) -> str: + raise NotImplementedError() + + +class RequestsPutToolWithParsing(BaseRequestsTool, BaseTool): + """Requests PUT tool with LLM-instructed extraction of truncated responses.""" + + name: str = "requests_put" + """Tool name.""" + description = REQUESTS_PUT_TOOL_DESCRIPTION + """Tool description.""" + response_length: Optional[int] = MAX_RESPONSE_LENGTH + """Maximum length of the response to be returned.""" + llm_chain: LLMChain = Field( + default_factory=_get_default_llm_chain_factory(PARSING_PUT_PROMPT) + ) + """LLMChain used to extract the response.""" + + def _run(self, text: str) -> str: + try: + data = json.loads(text) + except json.JSONDecodeError as e: + raise e + response = self.requests_wrapper.put(data["url"], data["data"]) + response = response[: self.response_length] + return self.llm_chain.predict( + response=response, instructions=data["output_instructions"] + ).strip() + + async def _arun(self, text: str) -> str: + raise NotImplementedError() + + +class RequestsDeleteToolWithParsing(BaseRequestsTool, BaseTool): + """A tool that sends a DELETE request and parses the response.""" + + name: str = "requests_delete" + """The name of the tool.""" + description = REQUESTS_DELETE_TOOL_DESCRIPTION + """The description of the tool.""" + + response_length: Optional[int] = MAX_RESPONSE_LENGTH + """The maximum length of the response.""" + llm_chain: LLMChain = Field( + default_factory=_get_default_llm_chain_factory(PARSING_DELETE_PROMPT) + ) + """The LLM chain used to parse the response.""" + + def _run(self, text: str) -> str: + try: + data = json.loads(text) + except json.JSONDecodeError as e: + raise e + response = self.requests_wrapper.delete(data["url"]) + response = response[: self.response_length] + return self.llm_chain.predict( + response=response, instructions=data["output_instructions"] + ).strip() + + async def _arun(self, text: str) -> str: + raise NotImplementedError() + + +# +# Orchestrator, planner, controller. +# +def _create_api_planner_tool( + api_spec: ReducedOpenAPISpec, llm: BaseLanguageModel +) -> Tool: + endpoint_descriptions = [ + f"{name} {description}" for name, description, _ in api_spec.endpoints + ] + prompt = PromptTemplate( + template=API_PLANNER_PROMPT, + input_variables=["query"], + partial_variables={"endpoints": "- " + "- ".join(endpoint_descriptions)}, + ) + chain = LLMChain(llm=llm, prompt=prompt) + tool = Tool( + name=API_PLANNER_TOOL_NAME, + description=API_PLANNER_TOOL_DESCRIPTION, + func=chain.run, + ) + return tool + + +def _create_api_controller_agent( + api_url: str, + api_docs: str, + requests_wrapper: RequestsWrapper, + llm: BaseLanguageModel, +) -> AgentExecutor: + get_llm_chain = LLMChain(llm=llm, prompt=PARSING_GET_PROMPT) + post_llm_chain = LLMChain(llm=llm, prompt=PARSING_POST_PROMPT) + tools: List[BaseTool] = [ + RequestsGetToolWithParsing( + requests_wrapper=requests_wrapper, llm_chain=get_llm_chain + ), + RequestsPostToolWithParsing( + requests_wrapper=requests_wrapper, llm_chain=post_llm_chain + ), + ] + prompt = PromptTemplate( + template=API_CONTROLLER_PROMPT, + input_variables=["input", "agent_scratchpad"], + partial_variables={ + "api_url": api_url, + "api_docs": api_docs, + "tool_names": ", ".join([tool.name for tool in tools]), + "tool_descriptions": "\n".join( + [f"{tool.name}: {tool.description}" for tool in tools] + ), + }, + ) + agent = ZeroShotAgent( + llm_chain=LLMChain(llm=llm, prompt=prompt), + allowed_tools=[tool.name for tool in tools], + ) + return AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True) + + +def _create_api_controller_tool( + api_spec: ReducedOpenAPISpec, + requests_wrapper: RequestsWrapper, + llm: BaseLanguageModel, +) -> Tool: + """Expose controller as a tool. + + The tool is invoked with a plan from the planner, and dynamically + creates a controller agent with relevant documentation only to + constrain the context. + """ + + base_url = api_spec.servers[0]["url"] # TODO: do better. + + def _create_and_run_api_controller_agent(plan_str: str) -> str: + pattern = r"\b(GET|POST|PATCH|DELETE)\s+(/\S+)*" + matches = re.findall(pattern, plan_str) + endpoint_names = [ + "{method} {route}".format(method=method, route=route.split("?")[0]) + for method, route in matches + ] + endpoint_docs_by_name = {name: docs for name, _, docs in api_spec.endpoints} + docs_str = "" + for endpoint_name in endpoint_names: + found_match = False + for name, _, docs in api_spec.endpoints: + regex_name = re.compile(re.sub("\{.*?\}", ".*", name)) + if regex_name.match(endpoint_name): + found_match = True + docs_str += f"== Docs for {endpoint_name} == \n{yaml.dump(docs)}\n" + if not found_match: + raise ValueError(f"{endpoint_name} endpoint does not exist.") + docs_str += f"== Docs for {endpoint_name} == \n{yaml.dump(docs)}\n" + + agent = _create_api_controller_agent(base_url, docs_str, requests_wrapper, llm) + return agent.run(plan_str) + + return Tool( + name=API_CONTROLLER_TOOL_NAME, + func=_create_and_run_api_controller_agent, + description=API_CONTROLLER_TOOL_DESCRIPTION, + ) + + +def create_openapi_agent( + api_spec: ReducedOpenAPISpec, + requests_wrapper: RequestsWrapper, + llm: BaseLanguageModel, + shared_memory: Optional[ReadOnlySharedMemory] = None, + callback_manager: Optional[BaseCallbackManager] = None, + verbose: bool = True, + agent_executor_kwargs: Optional[Dict[str, Any]] = None, + **kwargs: Dict[str, Any], +) -> AgentExecutor: + """Instantiate OpenAI API planner and controller for a given spec. + + Inject credentials via requests_wrapper. + + We use a top-level "orchestrator" agent to invoke the planner and controller, + rather than a top-level planner + that invokes a controller with its plan. This is to keep the planner simple. + """ + tools = [ + _create_api_planner_tool(api_spec, llm), + _create_api_controller_tool(api_spec, requests_wrapper, llm), + ] + prompt = PromptTemplate( + template=API_ORCHESTRATOR_PROMPT, + input_variables=["input", "agent_scratchpad"], + partial_variables={ + "tool_names": ", ".join([tool.name for tool in tools]), + "tool_descriptions": "\n".join( + [f"{tool.name}: {tool.description}" for tool in tools] + ), + }, + ) + agent = ZeroShotAgent( + llm_chain=LLMChain(llm=llm, prompt=prompt, memory=shared_memory), + allowed_tools=[tool.name for tool in tools], + **kwargs, + ) + return AgentExecutor.from_agent_and_tools( + agent=agent, + tools=tools, + callback_manager=callback_manager, + verbose=verbose, + maxIterations=2, + **(agent_executor_kwargs or {}), + ) diff --git a/llm-server/app.py b/llm-server/app.py index 7dfe0771a..c7188ec65 100644 --- a/llm-server/app.py +++ b/llm-server/app.py @@ -15,7 +15,7 @@ from prompts.base import api_base_prompt, non_api_base_prompt from routes.workflow.workflow_service import run_workflow from routes.workflow.typings.run_workflow_input import WorkflowData -from utils.detect_multiple_intents import hasMultipleIntents, hasSingleIntent +from utils.detect_multiple_intents import hasSingleIntent app = Flask(__name__) diff --git a/llm-server/routes/workflow/hierarchical_planner.py b/llm-server/routes/workflow/hierarchical_planner.py index f81bf2350..6162b2cf0 100644 --- a/llm-server/routes/workflow/hierarchical_planner.py +++ b/llm-server/routes/workflow/hierarchical_planner.py @@ -4,7 +4,14 @@ from routes.workflow.load_openapi_spec import load_openapi_spec from langchain.requests import RequestsWrapper from langchain.llms.openai import OpenAI -from langchain.agents.agent_toolkits.openapi import planner + +# from langchain.agents.agent_toolkits.openapi import planner # This is a custom planner, because of issue in langchains current implementation of planner, we will track this +from api_caller import planner + + +import os + +PLAN_AND_EXECUTE_MODEL = os.getenv("PLAN_AND_EXECUTE_MODEL", "gpt-4") def create_and_run_openapi_agent( @@ -17,8 +24,11 @@ def create_and_run_openapi_agent( # Create RequestsWrapper with auth requests_wrapper: RequestsWrapper = RequestsWrapper(headers=headers) + print( + f"Using {PLAN_AND_EXECUTE_MODEL} for plan and execute agent, you can change it by setting PLAN_AND_EXECUTE_MODEL variable" + ) # Create OpenAPI agent - llm: OpenAI = OpenAI(model_name="gpt-4", temperature=0.0) + llm: OpenAI = OpenAI(model_name=PLAN_AND_EXECUTE_MODEL, temperature=0.0) agent = planner.create_openapi_agent(spec, requests_wrapper, llm) # Run agent on user query diff --git a/llm-server/routes/workflow/workflow_service.py b/llm-server/routes/workflow/workflow_service.py index 35f6c43d5..7df1bb4c5 100644 --- a/llm-server/routes/workflow/workflow_service.py +++ b/llm-server/routes/workflow/workflow_service.py @@ -45,35 +45,40 @@ def run_workflow(data: WorkflowData) -> Any: text = data.text swagger_src = data.swagger_url headers = data.headers or {} - # This will come from request payload later on when implementing multi-tenancy + # This will come from the request payload later on when implementing multi-tenancy namespace = "workflows" server_base_url = data.server_base_url if not text: return json.dumps({"error": "text is required"}), 400 - vector_store = get_vector_store(StoreOptions(namespace)) - # documents = vector_store.similarity_search(text) + try: + vector_store = get_vector_store(StoreOptions(namespace)) + (document, score) = vector_store.similarity_search_with_relevance_scores(text)[ + 0 + ] - (document, score) = vector_store.similarity_search_with_relevance_scores(text)[0] + if score > VECTOR_DB_THRESHOLD: + print( + f"Record '{document}' is highly similar with a similarity score of {score}" + ) + first_document_id = ( + ObjectId(document.metadata["workflow_id"]) if document else None + ) + record = mongo.workflows.find_one({"_id": first_document_id}) - if score > VECTOR_DB_THRESHOLD: - print( - f"Record '{document}' is highly similar with a similarity score of {document}" - ) - first_document_id = ( - ObjectId(document.metadata["workflow_id"]) if document else None - ) - record = mongo.workflows.find_one({"_id": first_document_id}) + result = run_openapi_operations( + record, swagger_src, text, headers, server_base_url + ) + return result - result = run_openapi_operations( - record, swagger_src, text, headers, server_base_url - ) - return result - else: - # call openapi spec - result = create_and_run_openapi_agent(swagger_src, text, headers) - return result + except Exception as e: + # Log the error, but continue with the rest of the code + print(f"Error fetching data from namespace '{namespace}': {str(e)}") + + # Call openapi spec even if an error occurred with Qdrant + result = create_and_run_openapi_agent(swagger_src, text, headers) + return result def run_openapi_operations( diff --git a/llm-server/utils/vector_db/get_vector_store.py b/llm-server/utils/vector_db/get_vector_store.py index 8570255e8..5c02cd899 100644 --- a/llm-server/utils/vector_db/get_vector_store.py +++ b/llm-server/utils/vector_db/get_vector_store.py @@ -30,6 +30,7 @@ def get_vector_store(options: StoreOptions) -> VectorStore: client = qdrant_client.QdrantClient( url=os.environ["QDRANT_URL"], prefer_grpc=True ) + vector_store = Qdrant( client, collection_name=options.namespace, embeddings=embedding ) From ef359b3f0e40895aa8b600e69d5530116762a2e4 Mon Sep 17 00:00:00 2001 From: codebanesr Date: Wed, 27 Sep 2023 01:33:34 +0300 Subject: [PATCH 5/8] Adding a more simpler logic for routing to workflows --- llm-server/app.py | 4 ++-- llm-server/requirements.txt | 8 -------- llm-server/utils/detect_multiple_intents.py | 4 ++-- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/llm-server/app.py b/llm-server/app.py index c7188ec65..79d3f340b 100644 --- a/llm-server/app.py +++ b/llm-server/app.py @@ -15,7 +15,7 @@ from prompts.base import api_base_prompt, non_api_base_prompt from routes.workflow.workflow_service import run_workflow from routes.workflow.typings.run_workflow_input import WorkflowData -from utils.detect_multiple_intents import hasSingleIntent +from utils.detect_multiple_intents import hasSingleIntent, hasMultipleIntents app = Flask(__name__) @@ -42,7 +42,7 @@ def handle(): return json.dumps({"error": "swagger_url is required"}), 400 try: - if not hasSingleIntent(swagger_url, text): + if hasMultipleIntents(text): result = run_workflow( WorkflowData(text, swagger_url, headers, server_base_url) ) diff --git a/llm-server/requirements.txt b/llm-server/requirements.txt index e176c849b..12b1d7bf6 100644 --- a/llm-server/requirements.txt +++ b/llm-server/requirements.txt @@ -60,11 +60,9 @@ manifest-ml==0.0.1 MarkupSafe==2.1.3 marshmallow==3.19.0 marshmallow-enum==1.5.1 -mccabe @ file:///opt/conda/conda-bld/mccabe_1644221741721/work more-itertools==10.1.0 multidict==6.0.4 murmurhash==1.0.9 -mypy @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_b7yrbsawof/croot/mypy_1668440220235/work mypy-extensions==0.4.3 nltk==3.8.1 numexpr==2.8.4 @@ -82,10 +80,7 @@ pinecone-client==2.2.2 portalocker==2.7.0 preshed==3.0.8 protobuf==4.24.2 -psutil @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_1310b568-21f4-4cb0-b0e3-2f3d31e39728k9coaga5/croots/recipe/psutil_1656431280844/work -pycodestyle @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_5b2mq44vl0/croot/pycodestyle_1674267228581/work pydantic==1.10.11 -pyflakes @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_d9_v8e0_nr/croot/pyflakes_1674165137080/work pymongo==4.5.0 python-dateutil==2.8.2 python-dotenv==1.0.0 @@ -111,8 +106,6 @@ tenacity==8.2.2 thinc==8.1.10 tiktoken==0.4.0 tokenizers==0.13.3 -toml @ file:///tmp/build/80754af9/toml_1616166611790/work -tomli @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_d0e5ffbf-5cf1-45be-8693-c5dff8108a2awhthtjlq/croots/recipe/tomli_1657175508477/work tqdm==4.65.0 transformers==4.30.2 typer==0.9.0 @@ -120,7 +113,6 @@ types-PyYAML==6.0.12.11 types-requests==2.31.0.2 types-urllib3==1.26.25.14 typing-inspect==0.9.0 -typing_extensions @ file:///private/var/folders/nz/j6p8yfhx1mv_0grj5xl4650h0000gp/T/abs_1fdywrbp_3/croot/typing_extensions_1690297474455/work urllib3==1.26.16 wasabi==1.1.2 Werkzeug==2.3.6 diff --git a/llm-server/utils/detect_multiple_intents.py b/llm-server/utils/detect_multiple_intents.py index d1d1f4dbb..2449e8272 100644 --- a/llm-server/utils/detect_multiple_intents.py +++ b/llm-server/utils/detect_multiple_intents.py @@ -71,7 +71,7 @@ def getSummaries(spec_source: str): operation = paths[path] for field in operation: if "summary" in operation[field]: - summaries.append(operation[field]["summary"]) + summaries.append(operation[field]["operationId"]) return summaries @@ -82,7 +82,7 @@ def hasSingleIntent(spec_source: str, user_requirement: str) -> bool: User: Here is a list of API summaries: {summaries} - Considering the user's request outlined below, is it possible to fulfill their requirement with just one of the API calls listed above? Please reply with either "YES" or "NO" + Can one of these api's suffice the users request? Please reply with either "YES" or "NO" with explanation User requirement: {user_requirement} From 5152169c0e621ba134dd8a278348020d6a74f15c Mon Sep 17 00:00:00 2001 From: codebanesr Date: Wed, 27 Sep 2023 02:38:52 +0300 Subject: [PATCH 6/8] Moving environment variables around --- llm-server/app.py | 18 +++++++++++++----- .../routes/workflow/load_openapi_spec.py | 1 - 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/llm-server/app.py b/llm-server/app.py index 79d3f340b..56a025867 100644 --- a/llm-server/app.py +++ b/llm-server/app.py @@ -16,12 +16,16 @@ from routes.workflow.workflow_service import run_workflow from routes.workflow.typings.run_workflow_input import WorkflowData from utils.detect_multiple_intents import hasSingleIntent, hasMultipleIntents +import os +from dotenv import load_dotenv +load_dotenv() + +shared_folder = os.getenv("SHARED_FOLDER", "/app/shared_data/") app = Flask(__name__) app.register_blueprint(workflow, url_prefix="/workflow") - ## TODO: Implement caching for the swagger file content (no need to load it everytime) @app.route("/handle", methods=["POST", "OPTIONS"]) def handle(): @@ -41,6 +45,12 @@ def handle(): if not swagger_url: return json.dumps({"error": "swagger_url is required"}), 400 + if swagger_url.startswith("https://"): + pass + else: + swagger_url = shared_folder + swagger_url + + print(f"swagger_url::{swagger_url}") try: if hasMultipleIntents(text): result = run_workflow( @@ -52,16 +62,14 @@ def handle(): raise e if swagger_url.startswith("https://"): - full_url = swagger_url - response = requests.get(full_url) + response = requests.get(swagger_url) if response.status_code == 200: swagger_text = response.text else: return json.dumps({"error": "Failed to fetch Swagger content"}), 500 else: - full_url = "/app/shared_data/" + swagger_url try: - with open(full_url, "r") as file: + with open(swagger_url, "r") as file: swagger_text = file.read() except FileNotFoundError: return json.dumps({"error": "File not found"}), 404 diff --git a/llm-server/routes/workflow/load_openapi_spec.py b/llm-server/routes/workflow/load_openapi_spec.py index a244f4459..625149335 100644 --- a/llm-server/routes/workflow/load_openapi_spec.py +++ b/llm-server/routes/workflow/load_openapi_spec.py @@ -37,7 +37,6 @@ def load_spec_from_url(url: str) -> Any: def load_spec_from_file(file_path: str) -> Any: file_extension = os.path.splitext(file_path)[1].lower() - file_path = "/app/shared_data/" + file_path if file_extension == ".json": with open(file_path, "r") as file: return json.load(file) From 2cbd3fda63fe6fdd72cf62d6130276da28a11859 Mon Sep 17 00:00:00 2001 From: codebanesr Date: Wed, 27 Sep 2023 13:25:00 +0300 Subject: [PATCH 7/8] Adding unbuffered logs, fixing response structure etc --- docker-compose.yml | 2 ++ llm-server/Dockerfile | 4 ++-- llm-server/routes/workflow/workflow_service.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 03f91bbb4..d8304ccc2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,8 @@ services: - opencopilot_network env_file: - llm-server/.env + ports: + - 8002:8002 depends_on: - mongodb - qdrant diff --git a/llm-server/Dockerfile b/llm-server/Dockerfile index 8a3afbaed..c35324c3b 100644 --- a/llm-server/Dockerfile +++ b/llm-server/Dockerfile @@ -15,5 +15,5 @@ COPY . /app/ EXPOSE 8002 -# Run app.py when the container launches -CMD ["python", "app.py"] +# -u To prevent log accumulation, execute app.py with unbuffered output as soon as the container starts up. +CMD ["python", "-u" ,"app.py"] diff --git a/llm-server/routes/workflow/workflow_service.py b/llm-server/routes/workflow/workflow_service.py index 7df1bb4c5..d49d83c54 100644 --- a/llm-server/routes/workflow/workflow_service.py +++ b/llm-server/routes/workflow/workflow_service.py @@ -78,7 +78,7 @@ def run_workflow(data: WorkflowData) -> Any: # Call openapi spec even if an error occurred with Qdrant result = create_and_run_openapi_agent(swagger_src, text, headers) - return result + return {"response": result} def run_openapi_operations( From cdea31a2e669818415d889c6788595c8ee0e17e7 Mon Sep 17 00:00:00 2001 From: codebanesr Date: Wed, 27 Sep 2023 16:24:08 +0300 Subject: [PATCH 8/8] Adding readme --- README.md | 6 ++++++ llm-server/.env.example | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 15692d92a..9fe36460c 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ git clone git@github.com:openchatai/OpenCopilot.git OPENAI_API_KEY=YOUR_TOKEN_HERE ``` +- gpt-4: Ideal for more complex tasks, but may have slower processing times. +- gpt-3.5-turbo-16k: This model is significantly faster compared to GPT-4. +```env +PLAN_AND_EXECUTE_MODEL=gpt-3.5-turbo-16k +``` + - After updating your API key, navigate to the repository folder and run the following command (for macOS or Linux): ``` diff --git a/llm-server/.env.example b/llm-server/.env.example index 6d7816bcb..959d3706c 100644 --- a/llm-server/.env.example +++ b/llm-server/.env.example @@ -17,4 +17,5 @@ LANGCHAIN_PROJECT="PROJECT_NAME_GOES_HERE" # use gpt-4 for more complicated tasks, but it is usually much slower than 3.5-turbo family # gpt-3.5-turbo-16k - this model is many times faster than gpt-4, gpt-4 is more accurate with self observation -PLAN_AND_EXECUTE_MODEL=gpt-3.5-turbo-16k \ No newline at end of file +PLAN_AND_EXECUTE_MODEL=gpt-3.5-turbo-16k +VECTOR_DB_THRESHOLD=0.88 \ No newline at end of file