action.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. from enum import Enum
  2. import re
  3. from typing import Optional, Any, Dict, List
  4. from pydantic import BaseModel, Field, model_validator
  5. import openapi_spec_validator
  6. from app.exceptions.exception import ValidateFailedError
  7. from app.schemas.tool.authentication import Authentication, AuthenticationType
  8. # This function code from the Open Source Project TaskingAI.
  9. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  10. def validate_openapi_schema(schema: Dict):
  11. try:
  12. openapi_spec_validator.validate(schema)
  13. # check exactly one server in the schema
  14. except Exception as e:
  15. if hasattr(e, "message"):
  16. raise ValidateFailedError(f"Invalid openapi schema: {e.message}")
  17. else:
  18. raise ValidateFailedError(f"Invalid openapi schema: {e}")
  19. if "servers" not in schema:
  20. raise ValidateFailedError("No server is found in action schema")
  21. if "paths" not in schema:
  22. raise ValidateFailedError("No paths is found in action schema")
  23. if len(schema["servers"]) != 1:
  24. raise ValidateFailedError("Exactly one server is allowed in action schema")
  25. # check each path method has a valid description and operationId
  26. for path, methods in schema["paths"].items():
  27. for method, details in methods.items():
  28. if not details.get("description") or not isinstance(details["description"], str):
  29. if details.get("summary") and isinstance(details["summary"], str):
  30. # use summary as its description
  31. details["description"] = details["summary"]
  32. else:
  33. raise ValidateFailedError(f"No description is found in {method} {path} in action schema")
  34. if len(details["description"]) > 512:
  35. raise ValidateFailedError(
  36. f"Description cannot be longer than 512 characters in {method} {path} in action schema"
  37. )
  38. if not details.get("operationId") or not isinstance(details["operationId"], str):
  39. raise ValidateFailedError(f"No operationId is found in {method} {path} in action schema")
  40. if len(details["operationId"]) > 128:
  41. raise ValidateFailedError(
  42. f"operationId cannot be longer than 128 characters in {method} {path} in action schema"
  43. )
  44. if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", details["operationId"]):
  45. raise ValidateFailedError(
  46. f'Invalid operationId {details["operationId"]} in {method} {path} in action schema'
  47. )
  48. return schema
  49. # ----------------------------
  50. # Create Action
  51. # POST /actions
  52. # This class utilizes code from the Open Source Project TaskingAI.
  53. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  54. class ActionBulkCreateRequest(BaseModel):
  55. openapi_schema: Dict = Field(
  56. ...,
  57. description="The action schema is compliant with the OpenAPI Specification. "
  58. "If there are multiple paths and methods in the schema, "
  59. "the server will create multiple actions whose schema only has exactly one path and one method",
  60. )
  61. authentication: Authentication = Field(
  62. Authentication(type=AuthenticationType.none), description="The action API authentication."
  63. )
  64. use_for_everyone: bool = Field(default=False)
  65. @model_validator(mode="before")
  66. def model_validator(cls, data: Any):
  67. openapi_schema = data.get("openapi_schema")
  68. validate_openapi_schema(openapi_schema)
  69. authentication = data.get("authentication")
  70. if authentication:
  71. Authentication.model_validate(authentication).encrypt()
  72. return data
  73. # ----------------------------
  74. # Update Action
  75. # POST /actions/{action_id}
  76. # This class utilizes code from the Open Source Project TaskingAI.
  77. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  78. class ActionUpdateRequest(BaseModel):
  79. openapi_schema: Optional[Dict] = Field(
  80. default=None,
  81. description="The action schema, which is compliant with the OpenAPI Specification. "
  82. "It should only have exactly one path and one method.",
  83. )
  84. authentication: Optional[Authentication] = Field(None, description="The action API authentication.")
  85. use_for_everyone: bool = Field(default=False)
  86. @model_validator(mode="before")
  87. def model_validator(cls, data: Any):
  88. if not any([(data.get(key) is not None) for key in ["use_for_everyone", "openapi_schema", "authentication"]]):
  89. raise ValidateFailedError("At least one field should be filled")
  90. openapi_schema = data.get("openapi_schema")
  91. if openapi_schema:
  92. validate_openapi_schema(openapi_schema)
  93. authentication = data.get("authentication")
  94. if authentication:
  95. Authentication.model_validate(authentication).encrypt()
  96. return data
  97. # ----------------------------
  98. # Run an Action
  99. # POST /actions/{action_id}/run
  100. # This class utilizes code from the Open Source Project TaskingAI.
  101. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  102. class ActionRunRequest(BaseModel):
  103. parameters: Optional[Dict[str, Any]] = Field(None)
  104. headers: Optional[Dict[str, Any]] = Field(None)
  105. # This class utilizes code from the Open Source Project TaskingAI.
  106. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  107. class ActionMethod(str, Enum):
  108. GET = "GET"
  109. POST = "POST"
  110. PUT = "PUT"
  111. DELETE = "DELETE"
  112. PATCH = "PATCH"
  113. # HEAD = "HEAD"
  114. # OPTIONS = "OPTIONS"
  115. # TRACE = "TRACE"
  116. NONE = "NONE"
  117. # This class utilizes code from the Open Source Project TaskingAI.
  118. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  119. class ActionParam(BaseModel):
  120. type: str
  121. description: str
  122. enum: Optional[List[str]] = None
  123. required: bool
  124. properties: Optional[Dict[str, Dict]] = None
  125. def is_single_value_enum(self):
  126. return self.enum and len(self.enum) == 1
  127. # This class utilizes code from the Open Source Project TaskingAI.
  128. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  129. class ActionBodyType(str, Enum):
  130. JSON = "JSON"
  131. FORM = "FORM"
  132. NONE = "NONE"
  133. # This class utilizes code from the Open Source Project TaskingAI.
  134. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  135. class ChatCompletionFunctionParametersProperty(BaseModel):
  136. type: str = Field(
  137. ...,
  138. pattern="^(string|number|integer|boolean|object)$",
  139. description="The type of the parameter.",
  140. )
  141. description: str = Field(
  142. "",
  143. max_length=256,
  144. description="The description of the parameter.",
  145. )
  146. properties: Optional[Dict] = Field(
  147. None,
  148. description="The properties of the parameters.",
  149. )
  150. enum: Optional[List[str]] = Field(
  151. None,
  152. description="The enum list of the parameter. Which is only allowed when type is 'string'.",
  153. )
  154. # This class utilizes code from the Open Source Project TaskingAI.
  155. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  156. class ChatCompletionFunctionParameters(BaseModel):
  157. type: str = Field(
  158. "object",
  159. Literal="object",
  160. description="The type of the parameters, which is always 'object'.",
  161. )
  162. properties: Dict[str, ChatCompletionFunctionParametersProperty] = Field(
  163. ...,
  164. description="The properties of the parameters.",
  165. )
  166. required: List[str] = Field(
  167. [],
  168. description="The required parameters.",
  169. )
  170. # This class utilizes code from the Open Source Project TaskingAI.
  171. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  172. class ChatCompletionFunction(BaseModel):
  173. name: str = Field(
  174. ...,
  175. description="The name of the function.",
  176. examples=["plus_a_and_b"],
  177. )
  178. description: str = Field(
  179. ...,
  180. description="The description of the function.",
  181. examples=["Add two numbers"],
  182. )
  183. parameters: ChatCompletionFunctionParameters = Field(
  184. ...,
  185. description="The function's parameters are represented as an object in JSON Schema format.",
  186. examples=[
  187. {
  188. "type": "object",
  189. "properties": {
  190. "a": {"type": "number", "description": "The first number"},
  191. "b": {"type": "number", "description": "The second number"},
  192. },
  193. "required": ["a", "b"],
  194. }
  195. ],
  196. )
  197. # This class utilizes code from the Open Source Project TaskingAI.
  198. # The original code can be found at: https://github.com/TaskingAI/TaskingAI
  199. class ActionRunRequest(BaseModel):
  200. parameters: Optional[Dict[str, Any]] = Field(None)
  201. headers: Optional[Dict[str, Any]] = Field(None)