├── agent ├── __init__.py └── base.py ├── models ├── __init__.py └── models.py ├── utils ├── __init__.py ├── _pydantic.py └── function_utils.py ├── prompts ├── __init__.py └── prompts.py ├── orchestrator ├── __init__.py └── orchestrator.py ├── .gitignore ├── fsm.png ├── pyproject.toml ├── README.md ├── main.py └── poetry.lock /agent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /prompts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /orchestrator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .env 3 | *pycache* 4 | .cache* -------------------------------------------------------------------------------- /fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sentient-engineering/multi-agent-fsm/HEAD/fsm.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "multi-agent-fsm" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["namsgit "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.12" 10 | openai = "^1.40.6" 11 | pydantic = "^2.8.2" 12 | colorama = "^0.4.6" 13 | 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /models/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from enum import Enum 3 | from typing import List, Optional 4 | 5 | # Global 6 | class State(str, Enum): 7 | PLAN = "plan" 8 | HELP = "help" 9 | COMPLETED = "completed" 10 | 11 | class Task(BaseModel): 12 | id: int 13 | description: str 14 | result: Optional[str] 15 | 16 | class Memory(BaseModel): 17 | objective: str 18 | current_state: State 19 | final_response: Optional[str] 20 | completed_tasks: Optional[List[Task]] 21 | current_task: Optional[Task] 22 | 23 | class Config: 24 | use_enum_values = True 25 | 26 | # Planner 27 | class PlannerInput(BaseModel): 28 | objective: str 29 | task_for_review: Optional[Task] 30 | completed_tasks: List[Task] 31 | 32 | class PlannerOutput(BaseModel): 33 | next_task: Optional[Task] 34 | is_complete: bool 35 | final_response: Optional[str] 36 | 37 | # Helper 38 | class HelperInput(BaseModel): 39 | task: Task 40 | 41 | class HelperOutput(BaseModel): 42 | completed_task: Task -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Structured JSON is all you need 2 | 3 | This project implements a multi-agent framework by modeling agents as Finite State Machines (FSM). [OpenAI's Structured JSON](https://platform.openai.com/docs/guides/structured-outputs/) offers a simpler way to create reliable agents without relying on complex beefy frameworks. 4 | 5 | ## Flow Diagram 6 | 7 | ![Flow Diagram](fsm.png) 8 | 9 | ## Overview 10 | 11 | The project uses a Planner-Helper model where: 12 | 13 | 1. The Planner agent breaks down complex objectives into manageable tasks. 14 | 2. The Helper agent completes individual tasks. 15 | 3. An Orchestrator manages the state transitions and coordinates between agents. 16 | 17 | You can add more agents like Planner & Helper. 18 | 19 | ## Key Components 20 | 21 | - `State`: Enum representing different states (PLAN, HELP, COMPLETED). Can add more states if needed. 22 | - `Orchestrator`: Manages the overall flow and state transitions. 23 | - `Agent`: Represents either a Planner or Helper, interacting with OpenAI's API. Can add more agents if needed. 24 | - `Memory`: Optionally Maintain the current state, task list, and other relevant information for orchestrator. Memory is extensible to add arbitrary fields as needed. 25 | 26 | ## How to Start the Project 27 | 28 | 1. Clone the repository: 29 | 30 | ``` 31 | git clone https://github.com/yourusername/structured-json-agents.git 32 | cd structured-json-agents 33 | ``` 34 | 35 | 2. Install dependencies: 36 | 37 | ``` 38 | poetry install 39 | ``` 40 | 41 | 3. Set up your OpenAI API key: 42 | 43 | ``` 44 | export OPENAI_API_KEY='your-api-key-here' 45 | ``` 46 | 47 | 4. Run the main script: 48 | ``` 49 | poetry run python main.py 50 | ``` 51 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from orchestrator.orchestrator import Orchestrator 2 | from models.models import State, PlannerInput, PlannerOutput, HelperInput, HelperOutput 3 | from agent.base import BaseAgent 4 | from prompts.prompts import planner_system_prompt, helper_system_prompt 5 | 6 | if __name__ == "__main__": 7 | # Define your agents using the base agent. 8 | planner_agent = BaseAgent( 9 | name = "planner", 10 | system_prompt = planner_system_prompt, 11 | input_format = PlannerInput, 12 | output_format = PlannerOutput, 13 | keep_message_history = False 14 | ) 15 | 16 | helper_agent = BaseAgent( 17 | name = "helper", 18 | system_prompt = helper_system_prompt, 19 | input_format = HelperInput, 20 | output_format = HelperOutput, 21 | keep_message_history = False 22 | ) 23 | 24 | # Define your state machine 25 | #TODO: Combine state_machine and state_agent_map and maybe also combine the upsta func in the same object. so state_machine is self contained 26 | state_machine = { 27 | State.PLAN: [State.HELP, State.COMPLETED], 28 | State.HELP: [State.PLAN], 29 | State.COMPLETED: [] 30 | } 31 | 32 | # Define your state machine using states and respective agents to be called for the state 33 | state_to_agent_map = { 34 | State.PLAN: planner_agent, 35 | State.HELP: helper_agent 36 | } 37 | 38 | # Call orchestrator with objective, initial state & state machine. 39 | orchestrator = Orchestrator( 40 | objective="Write a blog post on AI advancements in 2024", 41 | initial_state=State.PLAN, 42 | state_machine=state_machine, 43 | state_to_agent_map = state_to_agent_map 44 | ) 45 | 46 | final_response = orchestrator.run() 47 | 48 | print(f"Final Response: {final_response}") -------------------------------------------------------------------------------- /prompts/prompts.py: -------------------------------------------------------------------------------- 1 | planner_system_prompt = """You are an AI planner agent. You are tasked with breaking down a complex objective into manageable tasks and reviewing completed tasks. The tasks are actually executed by another Helper AI agent, so you need to thoroughly review the task results. Your input and output are structured as follows: 2 | 3 | Input: 4 | - objective: The main goal to be achieved 5 | - task_for_review: A recently completed task (if any) that needs to be reviewed 6 | - completed_tasks: A list of all tasks that have been completed so far 7 | 8 | Output: 9 | - next_task: The next task to be executed (if the objective is not yet complete) 10 | - is_complete: A boolean indicating whether the entire objective has been achieved 11 | - final_response: A summary of the completed work (only if the objective is complete) 12 | 13 | Your responsibilities: 14 | 1. Break down the objective into small simple tasks for other AI to perform. Ensure each task you create is clear, specific, and actionable. 15 | 2. If there's a task_for_review, evaluate its quality and relevance to the objective. 16 | - If the task is not done according to your standards, you can get the send the same task again for execution. 17 | - If the task is done, send the next_task to be completed. 18 | 3. If all the tasks have been done, set is_complete to True and provide a final_response for the objective by combining the results of all the task. Don't summarize but combine the tasks. 19 | """ 20 | 21 | helper_system_prompt = """You are an AI helper responsible for completing individual tasks as part of a larger objective. Your input and output are structured as follows: 22 | 23 | Input: 24 | - task: A single Task object containing an id and a description of the work to be done 25 | 26 | Output: 27 | - completed_task: The same Task object, with the 'result' field filled in with your work 28 | 29 | Your responsibilities: 30 | 1. Carefully read and understand the task description provided. 31 | 2. Execute the task to the best of your abilities, focusing solely on the specific task given. 32 | 3. Provide a clear, concise, and relevant result in the completed_task.result field. 33 | 4. Ensure your result is directly related to the task and contributes to the overall objective. 34 | 35 | Focus on providing high-quality, relevant results for each individual task you're given. 36 | """ 37 | -------------------------------------------------------------------------------- /utils/_pydantic.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Tuple, Union, get_args 2 | 3 | from pydantic import BaseModel 4 | from pydantic.version import VERSION as PYDANTIC_VERSION 5 | from typing_extensions import get_origin 6 | 7 | __all__ = ( 8 | "JsonSchemaValue", 9 | "model_dump", 10 | "model_dump_json", 11 | "type2schema", 12 | "evaluate_forwardref", 13 | ) 14 | 15 | PYDANTIC_V1 = PYDANTIC_VERSION.startswith("1.") 16 | 17 | if not PYDANTIC_V1: 18 | from pydantic import TypeAdapter 19 | from pydantic._internal._typing_extra import ( 20 | eval_type_lenient as evaluate_forwardref, 21 | ) 22 | from pydantic.json_schema import JsonSchemaValue 23 | 24 | def type2schema(t: Any) -> JsonSchemaValue: 25 | """Convert a type to a JSON schema 26 | 27 | Args: 28 | t (Type): The type to convert 29 | 30 | Returns: 31 | JsonSchemaValue: The JSON schema 32 | """ 33 | return TypeAdapter(t).json_schema() 34 | 35 | def model_dump(model: BaseModel) -> Dict[str, Any]: 36 | """Convert a pydantic model to a dict 37 | 38 | Args: 39 | model (BaseModel): The model to convert 40 | 41 | Returns: 42 | Dict[str, Any]: The dict representation of the model 43 | 44 | """ 45 | return model.model_dump() 46 | 47 | def model_dump_json(model: BaseModel) -> str: 48 | """Convert a pydantic model to a JSON string 49 | 50 | Args: 51 | model (BaseModel): The model to convert 52 | 53 | Returns: 54 | str: The JSON string representation of the model 55 | """ 56 | return model.model_dump_json() 57 | 58 | 59 | # Remove this once we drop support for pydantic 1.x 60 | else: # pragma: no cover 61 | from pydantic import TypeAdapter 62 | from pydantic.typing import ( 63 | evaluate_forwardref as evaluate_forwardref, # type: ignore[no-redef] 64 | ) 65 | 66 | JsonSchemaValue = Dict[str, Any] # type: ignore[misc] 67 | 68 | def type2schema(t: Any) -> JsonSchemaValue: 69 | """Convert a type to a JSON schema 70 | 71 | Args: 72 | t (Type): The type to convert 73 | 74 | Returns: 75 | JsonSchemaValue: The JSON schema 76 | """ 77 | if PYDANTIC_V1: 78 | if t is None: 79 | return {"type": "null"} 80 | elif get_origin(t) is Union: 81 | return {"anyOf": [type2schema(tt) for tt in get_args(t)]} 82 | elif get_origin(t) in [Tuple, tuple]: 83 | prefixItems = [type2schema(tt) for tt in get_args(t)] 84 | return { 85 | "maxItems": len(prefixItems), 86 | "minItems": len(prefixItems), 87 | "prefixItems": prefixItems, 88 | "type": "array", 89 | } 90 | 91 | d = TypeAdapter.json_schema(t) 92 | if "title" in d: 93 | d.pop("title") 94 | if "description" in d: 95 | d.pop("description") 96 | 97 | return d 98 | 99 | def model_dump(model: BaseModel) -> Dict[str, Any]: 100 | """Convert a pydantic model to a dict 101 | 102 | Args: 103 | model (BaseModel): The model to convert 104 | 105 | Returns: 106 | Dict[str, Any]: The dict representation of the model 107 | 108 | """ 109 | return model.dict() 110 | 111 | def model_dump_json(model: BaseModel) -> str: 112 | """Convert a pydantic model to a JSON string 113 | 114 | Args: 115 | model (BaseModel): The model to convert 116 | 117 | Returns: 118 | str: The JSON string representation of the model 119 | """ 120 | return model.json() 121 | -------------------------------------------------------------------------------- /agent/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Callable, List, Optional, Tuple, Type 3 | 4 | import openai 5 | from pydantic import BaseModel 6 | 7 | 8 | from utils.function_utils import get_function_schema 9 | 10 | class BaseAgent: 11 | def __init__( 12 | self, 13 | name: str, 14 | system_prompt: str, 15 | input_format: Type[BaseModel], 16 | output_format: Type[BaseModel], 17 | tools: Optional[List[Tuple[Callable, str]]] = None, 18 | keep_message_history: bool = True, 19 | ): 20 | # Metdata 21 | self.name = name 22 | 23 | # Messages 24 | self.system_prompt = system_prompt 25 | self._initialize_messages() 26 | self.keep_message_history = keep_message_history 27 | 28 | # Input-output format 29 | self.input_format = input_format 30 | self.output_format = output_format 31 | 32 | # Openai client 33 | self.client = openai.Client() 34 | 35 | # Tools 36 | self.tools_list = [] 37 | self.executable_functions_list = {} 38 | if tools: 39 | self._initialize_tools(tools) 40 | 41 | def _initialize_tools(self, tools: List[Tuple[Callable, str]]): 42 | for func, func_desc in tools: 43 | self.tools_list.append(get_function_schema(func, description=func_desc)) 44 | self.executable_functions_list[func.__name__] = func 45 | 46 | def _initialize_messages(self): 47 | self.messages = [{"role": "system", "content": self.system_prompt}] 48 | 49 | async def run(self, input_data: BaseModel) -> BaseModel: 50 | if not isinstance(input_data, self.input_format): 51 | raise ValueError(f"Input data must be of type {self.input_format.__name__}") 52 | 53 | # Handle message history. 54 | if not self.keep_message_history: 55 | self._initialize_messages() 56 | 57 | 58 | # TODO: add a max_turn here to prevent a inifinite fallout 59 | while True: 60 | # TODO: 61 | # 1. Replace this with litellm post structured json is supported. 62 | # 2. exeception handling while calling the client 63 | if len(self.tools_list) == 0: 64 | response = self.client.beta.chat.completions.parse( 65 | model="gpt-4o-2024-08-06", 66 | messages=self.messages, 67 | response_format=self.output_format, 68 | ) 69 | else: 70 | # print(self.tools_list) 71 | response = self.client.beta.chat.completions.parse( 72 | model="gpt-4o-2024-08-06", 73 | messages=self.messages, 74 | response_format=self.output_format, 75 | tool_choice="auto", 76 | tools=self.tools_list, 77 | ) 78 | response_message = response.choices[0].message 79 | # print(response_message) 80 | tool_calls = response_message.tool_calls 81 | 82 | if tool_calls: 83 | self.messages.append(response_message) 84 | for tool_call in tool_calls: 85 | await self._append_tool_response(tool_call) 86 | continue 87 | 88 | parsed_response_content: self.output_format = response_message.parsed 89 | return parsed_response_content 90 | 91 | async def _append_tool_response(self, tool_call): 92 | function_name = tool_call.function.name 93 | function_to_call = self.executable_functions_list[function_name] 94 | function_args = json.loads(tool_call.function.arguments) 95 | try: 96 | function_response = await function_to_call(**function_args) 97 | # print(function_response) 98 | self.messages.append( 99 | { 100 | "tool_call_id": tool_call.id, 101 | "role": "tool", 102 | "name": function_name, 103 | "content": str(function_response), 104 | } 105 | ) 106 | except Exception as e: 107 | print(f"Error occurred calling the tool {function_name}: {str(e)}") 108 | self.messages.append( 109 | { 110 | "tool_call_id": tool_call.id, 111 | "role": "tool", 112 | "name": function_name, 113 | "content": str( 114 | "The tool responded with an error {e}\n. Please try again with a different tool or modify the parameters of the tool", 115 | function_response, 116 | ), 117 | } 118 | ) 119 | -------------------------------------------------------------------------------- /orchestrator/orchestrator.py: -------------------------------------------------------------------------------- 1 | from models.models import Memory, State, Task, PlannerInput, PlannerOutput, HelperInput, HelperOutput 2 | from agent.base import Agent 3 | from colorama import Fore, init 4 | import textwrap 5 | 6 | init(autoreset=True) 7 | 8 | class Orchestrator: 9 | def __init__(self, objective:str, initial_state: State, state_machine: dict[State, list[State]],state_to_agent_map: dict[State, Agent]): 10 | self.state_to_agent_map = state_to_agent_map 11 | self.state_machine = state_machine 12 | self.current_state = initial_state 13 | self.memory = Memory( 14 | objective=objective, 15 | completed_tasks=[], 16 | current_task=None, 17 | final_response=None 18 | ) 19 | 20 | def run(self) -> str: 21 | while self.current_state != State.COMPLETED: 22 | self._handle_state() 23 | 24 | self._print_final_response() 25 | return self.memory.final_response 26 | 27 | def _handle_state(self): 28 | current_state = self.current_state 29 | if current_state not in self.state_to_agent_map: 30 | raise ValueError(f"Unhandled state! No agent for {current_state}") 31 | 32 | if current_state == State.PLAN: 33 | self._handle_plan_state() 34 | elif current_state == State.HELP: 35 | self._handle_help_state() 36 | else: 37 | raise ValueError(f"Unhandled state: {current_state}") 38 | 39 | def _handle_plan_state(self): 40 | agent = self.state_to_agent_map[State.PLAN] 41 | self._print_memory_and_agent(agent.name) 42 | 43 | input_data = PlannerInput( 44 | objective=self.memory.objective, 45 | task_for_review=self.memory.current_task, 46 | completed_tasks=self.memory.completed_tasks 47 | ) 48 | 49 | output = agent.run(input_data) 50 | 51 | # Update memory 52 | self._update_memory_from_planner(output) 53 | 54 | print(f"{Fore.MAGENTA}Planner has updated the memory.") 55 | 56 | 57 | def _handle_help_state(self): 58 | agent = self.state_to_agent_map[State.HELP] 59 | self._print_memory_and_agent(agent.name) 60 | 61 | input_data = HelperInput(task=self.memory.current_task) 62 | 63 | output: HelperOutput = agent.run(input_data) 64 | 65 | self._print_task_result(output.completed_task) 66 | 67 | self._update_memory_from_helper(output) 68 | 69 | print(f"{Fore.MAGENTA}Helper has completed a task.") 70 | 71 | 72 | 73 | def _update_memory_from_planner(self, planner_output: PlannerOutput): 74 | if planner_output.is_complete: 75 | self.current_state = State.COMPLETED 76 | self.memory.final_response = planner_output.final_response 77 | elif planner_output.next_task: 78 | self.current_state = State.HELP 79 | next_task_id = len(self.memory.completed_tasks) + 1 80 | self.memory.current_task = Task(id=next_task_id, description = planner_output.next_task.description, result=None) 81 | else: 82 | raise ValueError("Planner did not provide next task or completion status") 83 | 84 | 85 | def _update_memory_from_helper(self, helper_output: HelperOutput): 86 | self.memory.completed_tasks.append(helper_output.completed_task) 87 | self.memory.current_task = None 88 | self.current_state = State.PLAN 89 | print("naman") 90 | print(self.memory.completed_tasks) 91 | 92 | def _print_memory_and_agent(self, agent_type: str): 93 | print(f"{Fore.CYAN}{'='*50}") 94 | print(f"{Fore.YELLOW}Current State: {Fore.GREEN}{self.memory.current_state}") 95 | print(f"{Fore.YELLOW}Agent: {Fore.GREEN}{agent_type}") 96 | if self.memory.current_task: 97 | print( 98 | f"{Fore.YELLOW}Current Task: {Fore.GREEN}{self.memory.current_task.description}" 99 | ) 100 | if len(self.memory.completed_tasks) == 0: 101 | print(f"{Fore.YELLOW}Completed Tasks:{Fore.GREEN} none") 102 | else: 103 | print(f"{Fore.YELLOW}Completed Tasks:") 104 | for task in self.memory.completed_tasks: 105 | status = "✓" if task.result else " " 106 | print(f"{Fore.GREEN} [{status}] {task.description}") 107 | print(f"{Fore.CYAN}{'='*50}") 108 | 109 | def _print_task_result(self, task: Task): 110 | print(f"{Fore.CYAN}{'='*50}") 111 | print(f"{Fore.YELLOW}Completed Task: {Fore.GREEN}{task.description}") 112 | print(f"{Fore.YELLOW}Result:") 113 | wrapped_result = textwrap.wrap(task.result, width=80) 114 | for line in wrapped_result: 115 | print(f"{Fore.WHITE}{line}") 116 | print(f"{Fore.CYAN}{'='*50}") 117 | 118 | def _print_final_response(self): 119 | print(f"\n{Fore.GREEN}{'='*50}") 120 | print(f"{Fore.GREEN}Objective Completed!") 121 | print(f"{Fore.GREEN}{'='*50}") 122 | print(f"{Fore.YELLOW}Final Response:") 123 | wrapped_response = textwrap.wrap(self.memory.final_response, width=80) 124 | for line in wrapped_response: 125 | print(f"{Fore.WHITE}{line}") 126 | print(f"{Fore.GREEN}{'='*50}") 127 | -------------------------------------------------------------------------------- /utils/function_utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | import json 4 | from logging import getLogger 5 | from typing import ( 6 | Any, 7 | Callable, 8 | Dict, 9 | ForwardRef, 10 | List, 11 | Optional, 12 | Set, 13 | Tuple, 14 | Type, 15 | TypeVar, 16 | Union, 17 | ) 18 | 19 | from pydantic import BaseModel, Field 20 | from typing_extensions import Annotated, Literal, get_args, get_origin 21 | 22 | from ._pydantic import ( 23 | JsonSchemaValue, 24 | evaluate_forwardref, 25 | model_dump, 26 | model_dump_json, 27 | type2schema, 28 | ) 29 | 30 | logger = getLogger(__name__) 31 | 32 | T = TypeVar("T") 33 | 34 | 35 | def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: 36 | """Get the type annotation of a parameter. 37 | 38 | Args: 39 | annotation: The annotation of the parameter 40 | globalns: The global namespace of the function 41 | 42 | Returns: 43 | The type annotation of the parameter 44 | """ 45 | if isinstance(annotation, str): 46 | annotation = ForwardRef(annotation) 47 | annotation = evaluate_forwardref(annotation, globalns, globalns) 48 | return annotation 49 | 50 | 51 | def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: 52 | """Get the signature of a function with type annotations. 53 | 54 | Args: 55 | call: The function to get the signature for 56 | 57 | Returns: 58 | The signature of the function with type annotations 59 | """ 60 | signature = inspect.signature(call) 61 | globalns = getattr(call, "__globals__", {}) 62 | typed_params = [ 63 | inspect.Parameter( 64 | name=param.name, 65 | kind=param.kind, 66 | default=param.default, 67 | annotation=get_typed_annotation(param.annotation, globalns), 68 | ) 69 | for param in signature.parameters.values() 70 | ] 71 | typed_signature = inspect.Signature(typed_params) 72 | return typed_signature 73 | 74 | 75 | def get_typed_return_annotation(call: Callable[..., Any]) -> Any: 76 | """Get the return annotation of a function. 77 | 78 | Args: 79 | call: The function to get the return annotation for 80 | 81 | Returns: 82 | The return annotation of the function 83 | """ 84 | signature = inspect.signature(call) 85 | annotation = signature.return_annotation 86 | 87 | if annotation is inspect.Signature.empty: 88 | return None 89 | 90 | globalns = getattr(call, "__globals__", {}) 91 | return get_typed_annotation(annotation, globalns) 92 | 93 | 94 | def get_param_annotations( 95 | typed_signature: inspect.Signature, 96 | ) -> Dict[str, Union[Annotated[Type[Any], str], Type[Any]]]: 97 | """Get the type annotations of the parameters of a function 98 | 99 | Args: 100 | typed_signature: The signature of the function with type annotations 101 | 102 | Returns: 103 | A dictionary of the type annotations of the parameters of the function 104 | """ 105 | return { 106 | k: v.annotation 107 | for k, v in typed_signature.parameters.items() 108 | if v.annotation is not inspect.Signature.empty 109 | } 110 | 111 | 112 | class Parameters(BaseModel): 113 | """Parameters of a function as defined by the OpenAI API""" 114 | 115 | type: Literal["object"] = "object" 116 | properties: Dict[str, JsonSchemaValue] 117 | required: List[str] 118 | additionalProperties: bool 119 | additionalProperties: bool 120 | 121 | 122 | class Function(BaseModel): 123 | """A function as defined by the OpenAI API""" 124 | 125 | description: Annotated[str, Field(description="Description of the function")] 126 | name: Annotated[str, Field(description="Name of the function")] 127 | parameters: Annotated[Parameters, Field(description="Parameters of the function")] 128 | strict: bool 129 | 130 | 131 | class ToolFunction(BaseModel): 132 | """A function under tool as defined by the OpenAI API.""" 133 | 134 | type: Literal["function"] = "function" 135 | function: Annotated[Function, Field(description="Function under tool")] 136 | 137 | 138 | def get_parameter_json_schema( 139 | k: str, v: Any, default_values: Dict[str, Any] 140 | ) -> JsonSchemaValue: 141 | def type2description(k: str, v: Union[Annotated[Type[Any], str], Type[Any]]) -> str: 142 | if get_origin(v) is Annotated: 143 | args = get_args(v) 144 | if len(args) > 1 and isinstance(args[1], str): 145 | return args[1] 146 | return k 147 | 148 | schema = type2schema(v) 149 | schema["description"] = type2description(k, v) 150 | 151 | if schema["type"] == "object": 152 | schema["additionalProperties"] = False 153 | if "properties" not in schema: 154 | schema["properties"] = {} 155 | 156 | if schema["type"] == "array": 157 | if "items" not in schema: 158 | schema["items"] = { 159 | "type": "object", 160 | "properties": {}, 161 | "additionalProperties": False, 162 | } 163 | elif schema["items"].get("type") == "object": 164 | if "properties" not in schema["items"]: 165 | schema["items"]["properties"] = {} 166 | schema["items"]["additionalProperties"] = False 167 | 168 | return schema 169 | 170 | 171 | def get_required_params(typed_signature: inspect.Signature) -> List[str]: 172 | """Get the required parameters of a function 173 | 174 | Args: 175 | signature: The signature of the function as returned by inspect.signature 176 | 177 | Returns: 178 | A list of the required parameters of the function 179 | """ 180 | return [ 181 | k 182 | for k, v in typed_signature.parameters.items() 183 | if v.default == inspect.Signature.empty 184 | ] 185 | 186 | 187 | def get_default_values(typed_signature: inspect.Signature) -> Dict[str, Any]: 188 | """Get default values of parameters of a function 189 | 190 | Args: 191 | signature: The signature of the function as returned by inspect.signature 192 | 193 | Returns: 194 | A dictionary of the default values of the parameters of the function 195 | """ 196 | return { 197 | k: v.default 198 | for k, v in typed_signature.parameters.items() 199 | if v.default != inspect.Signature.empty 200 | } 201 | 202 | 203 | def get_parameters( 204 | required: List[str], 205 | param_annotations: Dict[str, Union[Annotated[Type[Any], str], Type[Any]]], 206 | default_values: Dict[str, Any], 207 | ) -> Parameters: 208 | properties = {} 209 | for k, v in param_annotations.items(): 210 | if v is not inspect.Signature.empty: 211 | if get_origin(v) is Annotated: 212 | v_type = get_args(v)[0] 213 | v_desc = get_args(v)[1] if len(get_args(v)) > 1 else k 214 | else: 215 | v_type = v 216 | v_desc = k 217 | 218 | if get_origin(v_type) is List: 219 | item_type = get_args(v_type)[0] 220 | properties[k] = { 221 | "type": "array", 222 | "items": get_parameter_json_schema(k, item_type, default_values), 223 | "description": v_desc, 224 | } 225 | else: 226 | properties[k] = get_parameter_json_schema(k, v_type, default_values) 227 | properties[k]["description"] = v_desc 228 | 229 | return Parameters( 230 | properties=properties, 231 | required=list(properties.keys()), # All properties are required 232 | additionalProperties=False, 233 | ) 234 | 235 | 236 | def get_missing_annotations( 237 | typed_signature: inspect.Signature, required: List[str] 238 | ) -> Tuple[Set[str], Set[str]]: 239 | """Get the missing annotations of a function 240 | 241 | Ignores the parameters with default values as they are not required to be annotated, but logs a warning. 242 | Args: 243 | typed_signature: The signature of the function with type annotations 244 | required: The required parameters of the function 245 | 246 | Returns: 247 | A set of the missing annotations of the function 248 | """ 249 | all_missing = { 250 | k 251 | for k, v in typed_signature.parameters.items() 252 | if v.annotation is inspect.Signature.empty 253 | } 254 | missing = all_missing.intersection(set(required)) 255 | unannotated_with_default = all_missing.difference(missing) 256 | return missing, unannotated_with_default 257 | 258 | 259 | def get_function_schema( 260 | f: Callable[..., Any], *, name: Optional[str] = None, description: str 261 | ) -> Dict[str, Any]: 262 | """Get a JSON schema for a function as defined by the OpenAI API 263 | 264 | Args: 265 | f: The function to get the JSON schema for 266 | name: The name of the function 267 | description: The description of the function 268 | 269 | Returns: 270 | A JSON schema for the function 271 | 272 | Raises: 273 | TypeError: If the function is not annotated 274 | 275 | Examples: 276 | 277 | ```python 278 | def f(a: Annotated[str, "Parameter a"], b: int = 2, c: Annotated[float, "Parameter c"] = 0.1) -> None: 279 | pass 280 | 281 | get_function_schema(f, description="function f") 282 | 283 | # {'type': 'function', 284 | # 'function': {'description': 'function f', 285 | # 'name': 'f', 286 | # 'parameters': {'type': 'object', 287 | # 'properties': {'a': {'type': 'str', 'description': 'Parameter a'}, 288 | # 'b': {'type': 'int', 'description': 'b'}, 289 | # 'c': {'type': 'float', 'description': 'Parameter c'}}, 290 | # 'required': ['a']}}} 291 | ``` 292 | 293 | """ 294 | typed_signature = get_typed_signature(f) 295 | required = get_required_params(typed_signature) 296 | default_values = get_default_values(typed_signature) 297 | param_annotations = get_param_annotations(typed_signature) 298 | return_annotation = get_typed_return_annotation(f) 299 | missing, unannotated_with_default = get_missing_annotations( 300 | typed_signature, required 301 | ) 302 | 303 | if return_annotation is None: 304 | logger.warning( 305 | f"The return type of the function '{f.__name__}' is not annotated. Although annotating it is " 306 | + "optional, the function should return either a string, a subclass of 'pydantic.BaseModel'." 307 | ) 308 | 309 | if unannotated_with_default != set(): 310 | unannotated_with_default_s = [ 311 | f"'{k}'" for k in sorted(unannotated_with_default) 312 | ] 313 | logger.warning( 314 | f"The following parameters of the function '{f.__name__}' with default values are not annotated: " 315 | + f"{', '.join(unannotated_with_default_s)}." 316 | ) 317 | 318 | if missing != set(): 319 | missing_s = [f"'{k}'" for k in sorted(missing)] 320 | raise TypeError( 321 | f"All parameters of the function '{f.__name__}' without default values must be annotated. " 322 | + f"The annotations are missing for the following parameters: {', '.join(missing_s)}" 323 | ) 324 | 325 | fname = name if name else f.__name__ 326 | 327 | parameters = get_parameters( 328 | required, param_annotations, default_values=default_values 329 | ) 330 | 331 | function = ToolFunction( 332 | function=Function( 333 | description=description, 334 | name=fname, 335 | parameters=parameters, 336 | strict=True, 337 | ) 338 | ) 339 | 340 | schema = model_dump(function) 341 | 342 | return schema 343 | 344 | 345 | def get_load_param_if_needed_function( 346 | t: Any, 347 | ) -> Optional[Callable[[Dict[str, Any], Type[BaseModel]], BaseModel]]: 348 | """Get a function to load a parameter if it is a Pydantic model 349 | 350 | Args: 351 | t: The type annotation of the parameter 352 | 353 | Returns: 354 | A function to load the parameter if it is a Pydantic model, otherwise None 355 | 356 | """ 357 | if get_origin(t) is Annotated: 358 | return get_load_param_if_needed_function(get_args(t)[0]) 359 | 360 | def load_base_model(v: Dict[str, Any], t: Type[BaseModel]) -> BaseModel: 361 | return t(**v) 362 | 363 | return load_base_model if isinstance(t, type) and issubclass(t, BaseModel) else None 364 | 365 | 366 | def load_basemodels_if_needed(func: Callable[..., Any]) -> Callable[..., Any]: 367 | """A decorator to load the parameters of a function if they are Pydantic models 368 | 369 | Args: 370 | func: The function with annotated parameters 371 | 372 | Returns: 373 | A function that loads the parameters before calling the original function 374 | 375 | """ 376 | # get the type annotations of the parameters 377 | typed_signature = get_typed_signature(func) 378 | param_annotations = get_param_annotations(typed_signature) 379 | 380 | # get functions for loading BaseModels when needed based on the type annotations 381 | kwargs_mapping_with_nones = { 382 | k: get_load_param_if_needed_function(t) for k, t in param_annotations.items() 383 | } 384 | 385 | # remove the None values 386 | kwargs_mapping = { 387 | k: f for k, f in kwargs_mapping_with_nones.items() if f is not None 388 | } 389 | 390 | # a function that loads the parameters before calling the original function 391 | @functools.wraps(func) 392 | def _load_parameters_if_needed(*args: Any, **kwargs: Any) -> Any: 393 | # load the BaseModels if needed 394 | for k, f in kwargs_mapping.items(): 395 | kwargs[k] = f(kwargs[k], param_annotations[k]) 396 | 397 | # call the original function 398 | return func(*args, **kwargs) 399 | 400 | @functools.wraps(func) 401 | async def _a_load_parameters_if_needed(*args: Any, **kwargs: Any) -> Any: 402 | # load the BaseModels if needed 403 | for k, f in kwargs_mapping.items(): 404 | kwargs[k] = f(kwargs[k], param_annotations[k]) 405 | 406 | # call the original function 407 | return await func(*args, **kwargs) 408 | 409 | if inspect.iscoroutinefunction(func): 410 | return _a_load_parameters_if_needed 411 | else: 412 | return _load_parameters_if_needed 413 | 414 | 415 | def serialize_to_str(x: Any) -> str: 416 | if isinstance(x, str): 417 | return x 418 | elif isinstance(x, BaseModel): 419 | return model_dump_json(x) 420 | else: 421 | return json.dumps(x) 422 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.7.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 11 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.4.0" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, 22 | {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, 23 | ] 24 | 25 | [package.dependencies] 26 | idna = ">=2.8" 27 | sniffio = ">=1.1" 28 | 29 | [package.extras] 30 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 31 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 32 | trio = ["trio (>=0.23)"] 33 | 34 | [[package]] 35 | name = "certifi" 36 | version = "2024.7.4" 37 | description = "Python package for providing Mozilla's CA Bundle." 38 | optional = false 39 | python-versions = ">=3.6" 40 | files = [ 41 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 42 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 43 | ] 44 | 45 | [[package]] 46 | name = "colorama" 47 | version = "0.4.6" 48 | description = "Cross-platform colored terminal text." 49 | optional = false 50 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 51 | files = [ 52 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 53 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 54 | ] 55 | 56 | [[package]] 57 | name = "distro" 58 | version = "1.9.0" 59 | description = "Distro - an OS platform information API" 60 | optional = false 61 | python-versions = ">=3.6" 62 | files = [ 63 | {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, 64 | {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, 65 | ] 66 | 67 | [[package]] 68 | name = "h11" 69 | version = "0.14.0" 70 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 71 | optional = false 72 | python-versions = ">=3.7" 73 | files = [ 74 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 75 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 76 | ] 77 | 78 | [[package]] 79 | name = "httpcore" 80 | version = "1.0.5" 81 | description = "A minimal low-level HTTP client." 82 | optional = false 83 | python-versions = ">=3.8" 84 | files = [ 85 | {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, 86 | {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, 87 | ] 88 | 89 | [package.dependencies] 90 | certifi = "*" 91 | h11 = ">=0.13,<0.15" 92 | 93 | [package.extras] 94 | asyncio = ["anyio (>=4.0,<5.0)"] 95 | http2 = ["h2 (>=3,<5)"] 96 | socks = ["socksio (==1.*)"] 97 | trio = ["trio (>=0.22.0,<0.26.0)"] 98 | 99 | [[package]] 100 | name = "httpx" 101 | version = "0.27.0" 102 | description = "The next generation HTTP client." 103 | optional = false 104 | python-versions = ">=3.8" 105 | files = [ 106 | {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, 107 | {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, 108 | ] 109 | 110 | [package.dependencies] 111 | anyio = "*" 112 | certifi = "*" 113 | httpcore = "==1.*" 114 | idna = "*" 115 | sniffio = "*" 116 | 117 | [package.extras] 118 | brotli = ["brotli", "brotlicffi"] 119 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 120 | http2 = ["h2 (>=3,<5)"] 121 | socks = ["socksio (==1.*)"] 122 | 123 | [[package]] 124 | name = "idna" 125 | version = "3.7" 126 | description = "Internationalized Domain Names in Applications (IDNA)" 127 | optional = false 128 | python-versions = ">=3.5" 129 | files = [ 130 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 131 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 132 | ] 133 | 134 | [[package]] 135 | name = "jiter" 136 | version = "0.5.0" 137 | description = "Fast iterable JSON parser." 138 | optional = false 139 | python-versions = ">=3.8" 140 | files = [ 141 | {file = "jiter-0.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b599f4e89b3def9a94091e6ee52e1d7ad7bc33e238ebb9c4c63f211d74822c3f"}, 142 | {file = "jiter-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a063f71c4b06225543dddadbe09d203dc0c95ba352d8b85f1221173480a71d5"}, 143 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc0d5b8b3dd12e91dd184b87273f864b363dfabc90ef29a1092d269f18c7e28"}, 144 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c22541f0b672f4d741382a97c65609332a783501551445ab2df137ada01e019e"}, 145 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63314832e302cc10d8dfbda0333a384bf4bcfce80d65fe99b0f3c0da8945a91a"}, 146 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a25fbd8a5a58061e433d6fae6d5298777c0814a8bcefa1e5ecfff20c594bd749"}, 147 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503b2c27d87dfff5ab717a8200fbbcf4714516c9d85558048b1fc14d2de7d8dc"}, 148 | {file = "jiter-0.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d1f3d27cce923713933a844872d213d244e09b53ec99b7a7fdf73d543529d6d"}, 149 | {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c95980207b3998f2c3b3098f357994d3fd7661121f30669ca7cb945f09510a87"}, 150 | {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afa66939d834b0ce063f57d9895e8036ffc41c4bd90e4a99631e5f261d9b518e"}, 151 | {file = "jiter-0.5.0-cp310-none-win32.whl", hash = "sha256:f16ca8f10e62f25fd81d5310e852df6649af17824146ca74647a018424ddeccf"}, 152 | {file = "jiter-0.5.0-cp310-none-win_amd64.whl", hash = "sha256:b2950e4798e82dd9176935ef6a55cf6a448b5c71515a556da3f6b811a7844f1e"}, 153 | {file = "jiter-0.5.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4c8e1ed0ef31ad29cae5ea16b9e41529eb50a7fba70600008e9f8de6376d553"}, 154 | {file = "jiter-0.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6f16e21276074a12d8421692515b3fd6d2ea9c94fd0734c39a12960a20e85f3"}, 155 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5280e68e7740c8c128d3ae5ab63335ce6d1fb6603d3b809637b11713487af9e6"}, 156 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:583c57fc30cc1fec360e66323aadd7fc3edeec01289bfafc35d3b9dcb29495e4"}, 157 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26351cc14507bdf466b5f99aba3df3143a59da75799bf64a53a3ad3155ecded9"}, 158 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829df14d656b3fb87e50ae8b48253a8851c707da9f30d45aacab2aa2ba2d614"}, 159 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42a4bdcf7307b86cb863b2fb9bb55029b422d8f86276a50487982d99eed7c6e"}, 160 | {file = "jiter-0.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04d461ad0aebf696f8da13c99bc1b3e06f66ecf6cfd56254cc402f6385231c06"}, 161 | {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6375923c5f19888c9226582a124b77b622f8fd0018b843c45eeb19d9701c403"}, 162 | {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cec323a853c24fd0472517113768c92ae0be8f8c384ef4441d3632da8baa646"}, 163 | {file = "jiter-0.5.0-cp311-none-win32.whl", hash = "sha256:aa1db0967130b5cab63dfe4d6ff547c88b2a394c3410db64744d491df7f069bb"}, 164 | {file = "jiter-0.5.0-cp311-none-win_amd64.whl", hash = "sha256:aa9d2b85b2ed7dc7697597dcfaac66e63c1b3028652f751c81c65a9f220899ae"}, 165 | {file = "jiter-0.5.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9f664e7351604f91dcdd557603c57fc0d551bc65cc0a732fdacbf73ad335049a"}, 166 | {file = "jiter-0.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:044f2f1148b5248ad2c8c3afb43430dccf676c5a5834d2f5089a4e6c5bbd64df"}, 167 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:702e3520384c88b6e270c55c772d4bd6d7b150608dcc94dea87ceba1b6391248"}, 168 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:528d742dcde73fad9d63e8242c036ab4a84389a56e04efd854062b660f559544"}, 169 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf80e5fe6ab582c82f0c3331df27a7e1565e2dcf06265afd5173d809cdbf9ba"}, 170 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44dfc9ddfb9b51a5626568ef4e55ada462b7328996294fe4d36de02fce42721f"}, 171 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c451f7922992751a936b96c5f5b9bb9312243d9b754c34b33d0cb72c84669f4e"}, 172 | {file = "jiter-0.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:308fce789a2f093dca1ff91ac391f11a9f99c35369117ad5a5c6c4903e1b3e3a"}, 173 | {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7f5ad4a7c6b0d90776fdefa294f662e8a86871e601309643de30bf94bb93a64e"}, 174 | {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ea189db75f8eca08807d02ae27929e890c7d47599ce3d0a6a5d41f2419ecf338"}, 175 | {file = "jiter-0.5.0-cp312-none-win32.whl", hash = "sha256:e3bbe3910c724b877846186c25fe3c802e105a2c1fc2b57d6688b9f8772026e4"}, 176 | {file = "jiter-0.5.0-cp312-none-win_amd64.whl", hash = "sha256:a586832f70c3f1481732919215f36d41c59ca080fa27a65cf23d9490e75b2ef5"}, 177 | {file = "jiter-0.5.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f04bc2fc50dc77be9d10f73fcc4e39346402ffe21726ff41028f36e179b587e6"}, 178 | {file = "jiter-0.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f433a4169ad22fcb550b11179bb2b4fd405de9b982601914ef448390b2954f3"}, 179 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad4a6398c85d3a20067e6c69890ca01f68659da94d74c800298581724e426c7e"}, 180 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6baa88334e7af3f4d7a5c66c3a63808e5efbc3698a1c57626541ddd22f8e4fbf"}, 181 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ece0a115c05efca597c6d938f88c9357c843f8c245dbbb53361a1c01afd7148"}, 182 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:335942557162ad372cc367ffaf93217117401bf930483b4b3ebdb1223dbddfa7"}, 183 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649b0ee97a6e6da174bffcb3c8c051a5935d7d4f2f52ea1583b5b3e7822fbf14"}, 184 | {file = "jiter-0.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f4be354c5de82157886ca7f5925dbda369b77344b4b4adf2723079715f823989"}, 185 | {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5206144578831a6de278a38896864ded4ed96af66e1e63ec5dd7f4a1fce38a3a"}, 186 | {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8120c60f8121ac3d6f072b97ef0e71770cc72b3c23084c72c4189428b1b1d3b6"}, 187 | {file = "jiter-0.5.0-cp38-none-win32.whl", hash = "sha256:6f1223f88b6d76b519cb033a4d3687ca157c272ec5d6015c322fc5b3074d8a5e"}, 188 | {file = "jiter-0.5.0-cp38-none-win_amd64.whl", hash = "sha256:c59614b225d9f434ea8fc0d0bec51ef5fa8c83679afedc0433905994fb36d631"}, 189 | {file = "jiter-0.5.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0af3838cfb7e6afee3f00dc66fa24695199e20ba87df26e942820345b0afc566"}, 190 | {file = "jiter-0.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:550b11d669600dbc342364fd4adbe987f14d0bbedaf06feb1b983383dcc4b961"}, 191 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:489875bf1a0ffb3cb38a727b01e6673f0f2e395b2aad3c9387f94187cb214bbf"}, 192 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b250ca2594f5599ca82ba7e68785a669b352156260c5362ea1b4e04a0f3e2389"}, 193 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ea18e01f785c6667ca15407cd6dabbe029d77474d53595a189bdc813347218e"}, 194 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462a52be85b53cd9bffd94e2d788a09984274fe6cebb893d6287e1c296d50653"}, 195 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92cc68b48d50fa472c79c93965e19bd48f40f207cb557a8346daa020d6ba973b"}, 196 | {file = "jiter-0.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c834133e59a8521bc87ebcad773608c6fa6ab5c7a022df24a45030826cf10bc"}, 197 | {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab3a71ff31cf2d45cb216dc37af522d335211f3a972d2fe14ea99073de6cb104"}, 198 | {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cccd3af9c48ac500c95e1bcbc498020c87e1781ff0345dd371462d67b76643eb"}, 199 | {file = "jiter-0.5.0-cp39-none-win32.whl", hash = "sha256:368084d8d5c4fc40ff7c3cc513c4f73e02c85f6009217922d0823a48ee7adf61"}, 200 | {file = "jiter-0.5.0-cp39-none-win_amd64.whl", hash = "sha256:ce03f7b4129eb72f1687fa11300fbf677b02990618428934662406d2a76742a1"}, 201 | {file = "jiter-0.5.0.tar.gz", hash = "sha256:1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a"}, 202 | ] 203 | 204 | [[package]] 205 | name = "openai" 206 | version = "1.40.6" 207 | description = "The official Python library for the openai API" 208 | optional = false 209 | python-versions = ">=3.7.1" 210 | files = [ 211 | {file = "openai-1.40.6-py3-none-any.whl", hash = "sha256:b36372124a779381a420a34dd96f762baa748b6bdfaf83a6b9f2745f72ccc1c5"}, 212 | {file = "openai-1.40.6.tar.gz", hash = "sha256:2239232bcb7f4bd4ce8e02544b5769618582411cf399816d96686d1b6c1e5c8d"}, 213 | ] 214 | 215 | [package.dependencies] 216 | anyio = ">=3.5.0,<5" 217 | distro = ">=1.7.0,<2" 218 | httpx = ">=0.23.0,<1" 219 | jiter = ">=0.4.0,<1" 220 | pydantic = ">=1.9.0,<3" 221 | sniffio = "*" 222 | tqdm = ">4" 223 | typing-extensions = ">=4.11,<5" 224 | 225 | [package.extras] 226 | datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] 227 | 228 | [[package]] 229 | name = "pydantic" 230 | version = "2.8.2" 231 | description = "Data validation using Python type hints" 232 | optional = false 233 | python-versions = ">=3.8" 234 | files = [ 235 | {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, 236 | {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, 237 | ] 238 | 239 | [package.dependencies] 240 | annotated-types = ">=0.4.0" 241 | pydantic-core = "2.20.1" 242 | typing-extensions = [ 243 | {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, 244 | {version = ">=4.6.1", markers = "python_version < \"3.13\""}, 245 | ] 246 | 247 | [package.extras] 248 | email = ["email-validator (>=2.0.0)"] 249 | 250 | [[package]] 251 | name = "pydantic-core" 252 | version = "2.20.1" 253 | description = "Core functionality for Pydantic validation and serialization" 254 | optional = false 255 | python-versions = ">=3.8" 256 | files = [ 257 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, 258 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, 259 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, 260 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, 261 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, 262 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, 263 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, 264 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, 265 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, 266 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, 267 | {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, 268 | {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, 269 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, 270 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, 271 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, 272 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, 273 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, 274 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, 275 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, 276 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, 277 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, 278 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, 279 | {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, 280 | {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, 281 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, 282 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, 283 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, 284 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, 285 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, 286 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, 287 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, 288 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, 289 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, 290 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, 291 | {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, 292 | {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, 293 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, 294 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, 295 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, 296 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, 297 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, 298 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, 299 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, 300 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, 301 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, 302 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, 303 | {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, 304 | {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, 305 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, 306 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, 307 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, 308 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, 309 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, 310 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, 311 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, 312 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, 313 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, 314 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, 315 | {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, 316 | {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, 317 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, 318 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, 319 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, 320 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, 321 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, 322 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, 323 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, 324 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, 325 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, 326 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, 327 | {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, 328 | {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, 329 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, 330 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, 331 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, 332 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, 333 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, 334 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, 335 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, 336 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, 337 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, 338 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, 339 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, 340 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, 341 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, 342 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, 343 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, 344 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, 345 | {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, 346 | ] 347 | 348 | [package.dependencies] 349 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 350 | 351 | [[package]] 352 | name = "sniffio" 353 | version = "1.3.1" 354 | description = "Sniff out which async library your code is running under" 355 | optional = false 356 | python-versions = ">=3.7" 357 | files = [ 358 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 359 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 360 | ] 361 | 362 | [[package]] 363 | name = "tqdm" 364 | version = "4.66.5" 365 | description = "Fast, Extensible Progress Meter" 366 | optional = false 367 | python-versions = ">=3.7" 368 | files = [ 369 | {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, 370 | {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, 371 | ] 372 | 373 | [package.dependencies] 374 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 375 | 376 | [package.extras] 377 | dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] 378 | notebook = ["ipywidgets (>=6)"] 379 | slack = ["slack-sdk"] 380 | telegram = ["requests"] 381 | 382 | [[package]] 383 | name = "typing-extensions" 384 | version = "4.12.2" 385 | description = "Backported and Experimental Type Hints for Python 3.8+" 386 | optional = false 387 | python-versions = ">=3.8" 388 | files = [ 389 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 390 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 391 | ] 392 | 393 | [metadata] 394 | lock-version = "2.0" 395 | python-versions = "^3.12" 396 | content-hash = "8ea21d769714d083a22dae93ce6823f3db4d78e88677343f967d518a636059c7" 397 | --------------------------------------------------------------------------------