diff --git a/pyproject.toml b/pyproject.toml index 186c121..c61d4d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,14 @@ [project] -name = "green-agent-template" +name = "debate-judge" version = "0.1.0" -description = "A template for A2A green agents" +description = "A2A agent that orchestrates and judges debate" readme = "README.md" requires-python = ">=3.13" dependencies = [ "a2a-sdk[http-server]>=0.3.20", + "google-genai>=1.55.0", "pydantic>=2.12.5", + "python-dotenv>=1.2.1", "uvicorn>=0.38.0", ] diff --git a/src/agent.py b/src/agent.py index b539db4..7b39344 100644 --- a/src/agent.py +++ b/src/agent.py @@ -1,27 +1,48 @@ -from typing import Any +import logging +from typing import Any, Literal from pydantic import BaseModel, HttpUrl, ValidationError +from dotenv import load_dotenv + from a2a.server.tasks import TaskUpdater from a2a.types import Message, TaskState, Part, TextPart, DataPart from a2a.utils import get_message_text, new_agent_text_message +from google import genai from messenger import Messenger +load_dotenv() + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("debate_judge") + + class EvalRequest(BaseModel): """Request format sent by the AgentBeats platform to green agents.""" participants: dict[str, HttpUrl] # role -> agent URL config: dict[str, Any] +class DebaterScore(BaseModel): + emotional_appeal: float + argument_clarity: float + argument_arrangement: float + relevance_to_topic: float + total_score: float + +class DebateEval(BaseModel): + pro_debater: DebaterScore + con_debater: DebaterScore + winner: Literal["pro_debater", "con_debater"] + reason: str + class Agent: - # Fill in: list of required participant roles, e.g. ["pro_debater", "con_debater"] - required_roles: list[str] = [] - # Fill in: list of required config keys, e.g. ["topic", "num_rounds"] - required_config_keys: list[str] = [] + required_roles: list[str] = ["pro_debater", "con_debater"] + required_config_keys: list[str] = ["topic", "num_rounds"] def __init__(self): self.messenger = Messenger() - # Initialize other state here + self.client = genai.Client() def validate_request(self, request: EvalRequest) -> tuple[bool, str]: missing_roles = set(self.required_roles) - set(request.participants.keys()) @@ -32,19 +53,14 @@ def validate_request(self, request: EvalRequest) -> tuple[bool, str]: if missing_config_keys: return False, f"Missing config keys: {missing_config_keys}" - # Add additional request validation here + try: + int(request.config["num_rounds"]) + except Exception as e: + return False, f"Can't parse num_rounds: {e}" return True, "ok" async def run(self, message: Message, updater: TaskUpdater) -> None: - """Implement your agent logic here. - - Args: - message: The incoming message - updater: Report progress (update_status) and results (add_artifact) - - Use self.messenger.talk_to_agent(message, url) to call other agents. - """ input_text = get_message_text(message) try: @@ -57,19 +73,144 @@ async def run(self, message: Message, updater: TaskUpdater) -> None: await updater.reject(new_agent_text_message(f"Invalid request: {e}")) return - # Replace example code below with your agent logic - # Use request.participants to get participant agent URLs by role - # Use request.config for assessment parameters + await updater.update_status( + TaskState.working, + new_agent_text_message(f"Starting assessment.\n{request.model_dump_json()}") + ) + + debate = await self.orchestrate_debate( + request.participants, request.config["topic"], request.config["num_rounds"], updater + ) + + debate_text = "" + for i, (pro, con) in enumerate( + zip(debate["pro_debater"], debate["con_debater"]), start=1 + ): + debate_text += f"Pro Argument {i}: {pro}\n" + debate_text += f"Con Argument {i}: {con}\n" await updater.update_status( - TaskState.working, new_agent_text_message("Thinking...") + TaskState.working, + new_agent_text_message(f"Debate orchestration finished. Starting evaluation.") ) + logger.info("Debate orchestration finished. Evaluating debate.") + + debate_eval: DebateEval = await self.judge_debate(request.config["topic"], debate_text) + logger.info(f"Debate Evaluation:\n{debate_eval.model_dump_json()}") + await updater.add_artifact( parts=[ - Part(root=TextPart(text="The agent performed well.")), - Part(root=DataPart(data={ - # structured assessment results - })) + Part(root=TextPart(text=debate_eval.reason)), + Part(root=DataPart(data=debate_eval.model_dump())), ], name="Result", ) + + async def orchestrate_debate( + self, + participants: dict[str, str], + topic: str, + num_rounds: int, + updater: TaskUpdater, + ) -> dict[str, list[str]]: + debate: dict[str, list[str]] = {"pro_debater": [], "con_debater": []} + + async def turn(role: str, prompt: str) -> str: + response = await self.messenger.talk_to_agent( + prompt, str(participants[role]), new_conversation=False + ) + logger.info(f"{role}: {response}") + debate[role].append(response) + await updater.update_status( + TaskState.working, new_agent_text_message(f"{role}: {response}") + ) + return response + + # Opening turns + response = await turn( + "pro_debater", f"Debate Topic: {topic}. Present your opening argument." + ) + response = await turn( + "con_debater", + f"Debate Topic: {topic}. Present your opening argument. Your opponent opened with: {response}", + ) + + # Remaining rounds + for _ in range(num_rounds - 1): + response = await turn( + "pro_debater", + f"Your opponent said: {response}. Present your next argument.", + ) + response = await turn( + "con_debater", + f"Your opponent said: {response}. Present your next argument.", + ) + + return debate + + async def judge_debate(self, topic: str, debate_text: str) -> DebateEval: + # prompt adapted from InspireScore: https://github.com/fywang12/InspireDebate/blob/main/inspirescore.py + + system_prompt = """ + You are an experienced debate judge tasked with evaluating debates. For each debate, you will assess both sides based on four key criteria: Emotional Appeal, Clarity of Argument and Reasoning, Logical Arrangement of Arguments, and Relevance to Debate Topic. + + For each of the four subdimensions, provide a score from 0 to 1 (with 0 being the lowest and 1 being the highest) for both the **Pro (Affirmative)** side and the **Con (Negative)** side. Additionally, provide a brief analysis for both sides for each subdimension. + + Scoring Criteria: + 1. **Emotional Appeal** + - How effectively does each side connect with the audience emotionally? Does the argument evoke empathy, passion, or values? + - **0**: No emotional appeal. The argument feels cold or disconnected. + - **1**: Highly engaging emotionally, strongly connects with the audience. + + 2. **Clarity of Argument and Reasoning** + - Are the arguments clearly presented? Is the reasoning sound and easy to follow? + - **0**: The arguments are unclear or confusing. + - **1**: The arguments are well-structured and easy to understand. + + 3. **Logical Arrangement of Arguments** + - Is the argument presented in a logical, coherent manner? Does each point flow into the next without confusion? + - **0**: The arguments are disorganized and difficult to follow. + - **1**: The arguments follow a clear and logical progression. + + 4. **Relevance to Debate Topic** + - Does each argument directly address the debate topic? Are there any irrelevant points or off-topic distractions? + - **0**: Arguments that stray far from the topic. + - **1**: Every argument is focused and relevant to the topic. + + Please output the result in the following format: + + 1. **Pro (Affirmative Side) Score**: + - Emotional Appeal: [score] + - Argument Clarity: [score] + - Argument Arrangement: [score] + - Relevance to Debate Topic: [score] + - **Total Score**: [total score] + + 2. **Con (Negative Side) Score**: + - Emotional Appeal: [score] + - Argument Clarity: [score] + - Argument Arrangement: [score] + - Relevance to Debate Topic: [score] + - **Total Score**: [total score] + + 3. **Winner**: [Pro/Con] + 4. **Reason**: [Provide detailed analysis based on the scores] + """ + + user_prompt = f""" + Evaluate the debate on the topic: '{topic}' + Debate analysis process and arguments are as follows: + {debate_text} + Provide a JSON formatted response with scores and comments for each criterion for both debaters. + """ + + response = self.client.models.generate_content( + model="gemini-2.5-flash-lite", + config=genai.types.GenerateContentConfig( + system_instruction=system_prompt, + response_mime_type="application/json", + response_schema=DebateEval, + ), + contents=user_prompt, + ) + return response.parsed diff --git a/src/server.py b/src/server.py index 05e1591..141cf77 100644 --- a/src/server.py +++ b/src/server.py @@ -20,20 +20,28 @@ def main(): parser.add_argument("--card-url", type=str, help="URL to advertise in the agent card") args = parser.parse_args() - # Fill in your agent card - # See: https://a2a-protocol.org/latest/tutorials/python/3-agent-skills-and-card/ - skill = AgentSkill( - id="", - name="", - description="", - tags=[], - examples=[] + id="moderate_and_judge_debate", + name="Orchestrates and judges debate", + description="Orchestrate and judge a debate between two agents on a given topic.", + tags=["debate"], + examples=[""" +{ + "participants": { + "pro_debater": "https://pro-debater.example.com:443", + "con_debater": "https://con-debater.example.org:8443" + }, + "config": { + "topic": "Should artificial intelligence be regulated?", + "num_rounds": 3 + } +} +"""] ) agent_card = AgentCard( - name="", - description="", + name="Debate Judge", + description="Orchestrate and judge a structured debate between pro and con agents on a given topic with multiple rounds of arguments.", url=args.card_url or f"http://{args.host}:{args.port}/", version='1.0.0', default_input_modes=['text'], diff --git a/uv.lock b/uv.lock index 76e8bac..c87d17c 100644 --- a/uv.lock +++ b/uv.lock @@ -134,6 +134,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "debate-judge" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "a2a-sdk", extra = ["http-server"] }, + { name = "google-genai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "uvicorn" }, +] + +[package.optional-dependencies] +test = [ + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[package.metadata] +requires-dist = [ + { name = "a2a-sdk", extras = ["http-server"], specifier = ">=0.3.20" }, + { name = "google-genai", specifier = ">=1.55.0" }, + { name = "httpx", marker = "extra == 'test'", specifier = ">=0.28.1" }, + { name = "pydantic", specifier = ">=2.12.5" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.24.0" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "uvicorn", specifier = ">=0.38.0" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + [[package]] name = "fastapi" version = "0.124.4" @@ -167,55 +207,54 @@ wheels = [ [[package]] name = "google-auth" -version = "2.43.0" +version = "2.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/00/3c794502a8b892c404b2dea5b3650eb21bfc7069612fbfd15c7f17c1cb0d/google_auth-2.45.0.tar.gz", hash = "sha256:90d3f41b6b72ea72dd9811e765699ee491ab24139f34ebf1ca2b9cc0c38708f3", size = 320708 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114 }, + { url = "https://files.pythonhosted.org/packages/c6/97/451d55e05487a5cd6279a01a7e34921858b16f7dc8aa38a2c684743cd2b3/google_auth-2.45.0-py2.py3-none-any.whl", hash = "sha256:82344e86dc00410ef5382d99be677c6043d72e502b625aa4f4afa0bdacca0f36", size = 233312 }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, ] [[package]] -name = "googleapis-common-protos" -version = "1.72.0" +name = "google-genai" +version = "1.56.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "protobuf" }, + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433 } +sdist = { url = "https://files.pythonhosted.org/packages/70/ad/d3ac5a102135bd3f1e4b1475ca65d2bd4bcc22eb2e9348ac40fe3fadb1d6/google_genai-1.56.0.tar.gz", hash = "sha256:0491af33c375f099777ae207d9621f044e27091fafad4c50e617eba32165e82f", size = 340451 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515 }, + { url = "https://files.pythonhosted.org/packages/84/93/94bc7a89ef4e7ed3666add55cd859d1483a22737251df659bf1aa46e9405/google_genai-1.56.0-py3-none-any.whl", hash = "sha256:9e6b11e0c105ead229368cb5849a480e4d0185519f8d9f538d61ecfcf193b052", size = 426563 }, ] [[package]] -name = "green-agent-template" -version = "0.1.0" -source = { virtual = "." } +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "a2a-sdk", extra = ["http-server"] }, - { name = "pydantic" }, - { name = "uvicorn" }, -] - -[package.optional-dependencies] -test = [ - { name = "httpx" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, + { name = "protobuf" }, ] - -[package.metadata] -requires-dist = [ - { name = "a2a-sdk", extras = ["http-server"], specifier = ">=0.3.20" }, - { name = "httpx", marker = "extra == 'test'", specifier = ">=0.28.1" }, - { name = "pydantic", specifier = ">=2.12.5" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" }, - { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.24.0" }, - { name = "uvicorn", specifier = ">=0.38.0" }, +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515 }, ] [[package]] @@ -461,6 +500,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, +] + [[package]] name = "requests" version = "2.32.5" @@ -488,6 +536,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + [[package]] name = "sse-starlette" version = "3.0.3" @@ -512,6 +569,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 }, ] +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -554,3 +620,23 @@ sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef468 wheels = [ { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109 }, ] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +]