From 446fc53ff645071a1729591292510e98a1e2250b Mon Sep 17 00:00:00 2001 From: Keepingitneil Date: Tue, 9 Sep 2025 11:05:29 -0700 Subject: [PATCH 1/9] publish flow --- .github/workflows/publish-client-python.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-client-python.yml b/.github/workflows/publish-client-python.yml index b3537b97..b6bd5504 100644 --- a/.github/workflows/publish-client-python.yml +++ b/.github/workflows/publish-client-python.yml @@ -1,4 +1,4 @@ -name: Publish PyPI Package +name: Publish Client Python on: workflow_dispatch: @@ -29,8 +29,10 @@ jobs: # Build the package - name: Build package + working-directory: sdks/python run: uv build # Publish to PyPI - name: Publish to PyPI + working-directory: sdks/python run: uv publish \ No newline at end of file From c2b4b97fb66f604064fc288f8e550d2c00b2e38e Mon Sep 17 00:00:00 2001 From: Keepingitneil Date: Tue, 9 Sep 2025 11:09:16 -0700 Subject: [PATCH 2/9] use the published package --- sdks/python/example/pyproject.toml | 5 +---- sdks/python/example/uv.lock | 15 +++++---------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/sdks/python/example/pyproject.toml b/sdks/python/example/pyproject.toml index f37c7515..d55d212f 100644 --- a/sdks/python/example/pyproject.toml +++ b/sdks/python/example/pyproject.toml @@ -10,9 +10,6 @@ requires-python = "==3.12.2" dependencies = [ "aiofiles>=24.1.0", "aiohttp>=3.12.15", - "gabber-sdk", + "gabber-sdk>=0.2.0", "numpy>=2.3.2", ] - -[tool.uv.sources] -gabber-sdk = { path = "../", editable = true } diff --git a/sdks/python/example/uv.lock b/sdks/python/example/uv.lock index 526ba8d1..11af34fe 100644 --- a/sdks/python/example/uv.lock +++ b/sdks/python/example/uv.lock @@ -138,14 +138,14 @@ dependencies = [ requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, { name = "aiohttp", specifier = ">=3.12.15" }, - { name = "gabber-sdk", editable = "../" }, + { name = "gabber-sdk", specifier = ">=0.2.0" }, { name = "numpy", specifier = ">=2.3.2" }, ] [[package]] name = "gabber-sdk" version = "0.2.0" -source = { editable = "../" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "aiohttp-retry" }, @@ -153,14 +153,9 @@ dependencies = [ { name = "numpy" }, { name = "pydantic" }, ] - -[package.metadata] -requires-dist = [ - { name = "aiohttp", specifier = "~=3.0" }, - { name = "aiohttp-retry" }, - { name = "livekit", specifier = "~=1.0" }, - { name = "numpy", specifier = "~=2.3.2" }, - { name = "pydantic", specifier = "~=2.11" }, +sdist = { url = "https://files.pythonhosted.org/packages/c1/39/8abee69d27999f54eb8153bee4a1bc4b10b547c17a152d96847a2ba1b61d/gabber_sdk-0.2.0.tar.gz", hash = "sha256:c3747fdf620d7f8a1ef8b5a1a0011e6066ade38a66c34d19bb4024127601e92a", size = 80860, upload_time = "2025-09-09T18:08:08.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9d/a97424a1cb31ee5e3d54055085a45218769875ef2cdb6d71803f1f1dd74a/gabber_sdk-0.2.0-py3-none-any.whl", hash = "sha256:a12c3474ed7a81fb341f64e756a768f4927b5e777414cde516ac34d3a1d263e4", size = 14180, upload_time = "2025-09-09T18:08:08.126Z" }, ] [[package]] From 784af68b4cdc052f06441a204312466d4976392c Mon Sep 17 00:00:00 2001 From: Keepingitneil Date: Tue, 9 Sep 2025 15:03:35 -0700 Subject: [PATCH 3/9] just more cooking on mcp proxy --- engine/src/core/graph/runtime_api.py | 17 +- engine/src/main.py | 15 - engine/src/services/engine.py | 2 - engine/src/services/mcp_proxy_client.py | 18 - engine/src/services/repository/repository.py | 2 +- frontend/app/app/[app]/client_page.tsx | 3 +- frontend/app/debug/[run_id]/client_page.tsx | 7 +- .../app/example/[example]/client_page.tsx | 5 +- frontend/components/RunId.tsx | 34 + frontend/components/flow/FlowEdit.tsx | 6 + frontend/hooks/useRun.tsx | 19 +- frontend/lib/repository.ts | 12 +- mcp_proxy_client/pyproject.toml | 64 ++ .../src/__pycache__/proxy.cpython-312.pyc | Bin 0 -> 2118 bytes mcp_proxy_client/src/app.py | 31 + mcp_proxy_client/src/connection/__init__.py | 4 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 334 bytes .../connection_provider.cpython-312.pyc | Bin 0 -> 698 bytes .../local_connection_provider.cpython-312.pyc | Bin 0 -> 1702 bytes .../src/connection/connection_provider.py | 7 + .../connection/local_connection_provider.py | 14 + mcp_proxy_client/src/main.py | 41 + mcp_proxy_client/src/mcp_proxy/__init__.py | 0 .../src/mcp_proxy/datachannel_transport.py | 57 ++ mcp_proxy_client/src/mcp_proxy/sse_proxy.py | 12 + mcp_proxy_client/uv.lock | 707 ++++++++++++++++++ sdks/python/gabber/__init__.py | 6 + sdks/python/gabber/engine/engine.py | 78 +- 28 files changed, 1070 insertions(+), 91 deletions(-) delete mode 100644 engine/src/services/mcp_proxy_client.py create mode 100644 frontend/components/RunId.tsx create mode 100644 mcp_proxy_client/pyproject.toml create mode 100644 mcp_proxy_client/src/__pycache__/proxy.cpython-312.pyc create mode 100644 mcp_proxy_client/src/app.py create mode 100644 mcp_proxy_client/src/connection/__init__.py create mode 100644 mcp_proxy_client/src/connection/__pycache__/__init__.cpython-312.pyc create mode 100644 mcp_proxy_client/src/connection/__pycache__/connection_provider.cpython-312.pyc create mode 100644 mcp_proxy_client/src/connection/__pycache__/local_connection_provider.cpython-312.pyc create mode 100644 mcp_proxy_client/src/connection/connection_provider.py create mode 100644 mcp_proxy_client/src/connection/local_connection_provider.py create mode 100644 mcp_proxy_client/src/main.py create mode 100644 mcp_proxy_client/src/mcp_proxy/__init__.py create mode 100644 mcp_proxy_client/src/mcp_proxy/datachannel_transport.py create mode 100644 mcp_proxy_client/src/mcp_proxy/sse_proxy.py create mode 100644 mcp_proxy_client/uv.lock diff --git a/engine/src/core/graph/runtime_api.py b/engine/src/core/graph/runtime_api.py index 85f9a4c9..7bbcc356 100644 --- a/engine/src/core/graph/runtime_api.py +++ b/engine/src/core/graph/runtime_api.py @@ -13,6 +13,8 @@ from nodes.core.media.publish import Publish from nodes.core.tool import MCP +PING_BYTES = "ping".encode("utf-8") + class RuntimeApi: def __init__( @@ -80,13 +82,24 @@ def on_pad(p: pad.Pad, value: Any): p._add_update_handler(on_pad) def on_data(packet: rtc.DataPacket): - if not packet.topic or not packet.topic.startswith("runtime_api"): + logging.info(f"NEIL **************** Received data packet: {packet.data}") + if not packet.topic or packet.topic != "runtime_api": + logging.warning( + f"Unknown data packet topic: {packet.topic} - {packet.data}" + ) return - request = RuntimeRequest.model_validate_json(packet.data) + try: + request = RuntimeRequest.model_validate_json(packet.data) + except Exception as e: + logging.error(f"Invalid runtime_api request: {e}", exc_info=e) + return req_id = request.req_id ack_resp = RuntimeRequestAck(req_id=req_id, type="ack") complete_resp = RuntimeResponse(req_id=req_id, type="complete") + logging.info( + "NEIL ---------------- Handling runtime_api request: %s", request + ) dc_queue.put_nowait( QueueItem(payload=ack_resp, participant=packet.participant) diff --git a/engine/src/main.py b/engine/src/main.py index 902b8327..025b05f3 100644 --- a/engine/src/main.py +++ b/engine/src/main.py @@ -6,7 +6,6 @@ import logging import os -import aiohttp import click from pydantic import TypeAdapter @@ -149,19 +148,5 @@ def generate_statemachine_schema(): print(json.dumps(config_schema, indent=2)) -@main_cli.command("mcp-proxy-client") -def mcp_proxy(): - async def run_proxy_client(): - while True: - try: - pass - except Exception as e: - logging.error(f"Error in MCP proxy client: {e}", exc_info=True) - await asyncio.sleep(5) - continue - - asyncio.run(run_proxy_client()) - - if __name__ == "__main__": main_cli() diff --git a/engine/src/services/engine.py b/engine/src/services/engine.py index a38a73c8..2c47f932 100644 --- a/engine/src/services/engine.py +++ b/engine/src/services/engine.py @@ -58,9 +58,7 @@ def cpu_load_fnc(worker: agents.Worker) -> float: async def req_fnc(worker: agents.JobRequest): - logging.info("NEIL Requesting job for gabber-engine") await worker.accept(name="gabber-engine", identity="gabber-engine") - logging.info("NEIL Requesting job for gabber-engine 2") def run_engine( diff --git a/engine/src/services/mcp_proxy_client.py b/engine/src/services/mcp_proxy_client.py deleted file mode 100644 index 4e3ed656..00000000 --- a/engine/src/services/mcp_proxy_client.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved. -# SPDX-License-Identifier: SUL-1.0 - - -class MCPProxyClientService: - def __init__(self): - pass - - async def run(self): - pass - - -class MCPProxyClient: - def __init__(self, *, connection_token: str): - pass - - async def run(self): - pass diff --git a/engine/src/services/repository/repository.py b/engine/src/services/repository/repository.py index bf07165c..defeabbb 100644 --- a/engine/src/services/repository/repository.py +++ b/engine/src/services/repository/repository.py @@ -758,7 +758,7 @@ async def mcp_proxy_connection(self, request: aiohttp.web.Request): can_publish_data=True, can_subscribe=True, ) - ).with_identity("debug") + ).with_identity(f"mcp_proxy-{short_uuid()}") connection_details = models.AppRunConnectionDetails( url=livekit_url, diff --git a/frontend/app/app/[app]/client_page.tsx b/frontend/app/app/[app]/client_page.tsx index c9af4c57..d6c5fa50 100644 --- a/frontend/app/app/[app]/client_page.tsx +++ b/frontend/app/app/[app]/client_page.tsx @@ -38,8 +38,7 @@ export function ClientPage({ existingApp }: Props) { const startRunImpl = useCallback( async (params: { graph: GraphEditorRepresentation }) => { - const resp = await createAppRun({ graph: params.graph }); - return resp.connection_details; + return await createAppRun({ graph: params.graph }); }, [], ); diff --git a/frontend/app/debug/[run_id]/client_page.tsx b/frontend/app/debug/[run_id]/client_page.tsx index 137bb130..06319b2e 100644 --- a/frontend/app/debug/[run_id]/client_page.tsx +++ b/frontend/app/debug/[run_id]/client_page.tsx @@ -14,16 +14,17 @@ import { DebugGraph } from "@/components/debug/DebugGraph"; type Props = { connectionDetails: AppRunConnectionDetails; + runId: string; graph: GraphEditorRepresentation; }; -export function ClientPage({ graph, connectionDetails }: Props) { +export function ClientPage({ graph, connectionDetails, runId }: Props) { const saveImpl = useCallback(async () => { throw new Error("saveImpl is not implemented for Debug mode"); }, []); const startRunImpl = useCallback(async () => { - return connectionDetails; - }, [connectionDetails]); + return { connectionDetails, runId }; + }, [connectionDetails, runId]); return (
diff --git a/frontend/app/example/[example]/client_page.tsx b/frontend/app/example/[example]/client_page.tsx index c22883bc..789d6e29 100644 --- a/frontend/app/example/[example]/client_page.tsx +++ b/frontend/app/example/[example]/client_page.tsx @@ -25,9 +25,7 @@ export function ClientPage({ existingExample: existingApp }: Props) { const startRunImpl = useCallback( async (params: { graph: GraphEditorRepresentation }) => { - const resp = await createAppRun({ graph: params.graph }); - const connDetails = resp.connection_details; - return connDetails; + return await createAppRun({ graph: params.graph }); }, [], ); @@ -39,6 +37,7 @@ export function ClientPage({ existingExample: existingApp }: Props) { editor_url="ws://localhost:8000/ws" savedGraph={existingApp.graph} saveImpl={saveImpl} + debug={false} > diff --git a/frontend/components/RunId.tsx b/frontend/components/RunId.tsx new file mode 100644 index 00000000..0b27f663 --- /dev/null +++ b/frontend/components/RunId.tsx @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved. + * SPDX-License-Identifier: SUL-1.0 + */ + +import { useRun } from "@/hooks/useRun"; +import toast from "react-hot-toast"; + +export function RunId() { + const { runId } = useRun(); + + if (!runId) { + return null; + } + + const handleCopy = async () => { + if (navigator.clipboard) { + toast.success("Run ID copied to clipboard"); + await navigator.clipboard.writeText(runId); + } + }; + + return ( +
+
run_id:
+ +
+ ); +} diff --git a/frontend/components/flow/FlowEdit.tsx b/frontend/components/flow/FlowEdit.tsx index 9cb7ae2a..b4da662f 100644 --- a/frontend/components/flow/FlowEdit.tsx +++ b/frontend/components/flow/FlowEdit.tsx @@ -38,6 +38,7 @@ import { PadEditorRepresentation, PortalEnd, } from "@/generated/editor"; +import { RunId } from "../RunId"; const edgeTypes = { hybrid: HybridEdge, @@ -146,6 +147,11 @@ function FlowEditInner({ editable }: Props) { {connectionStatus}
)} + { +
+ +
+ }
void; startRun: (params: { graph: GraphEditorRepresentation }) => void; }; const RunContext = createContext(undefined); +type GenerateConnectionDetails = (params: { + graph: GraphEditorRepresentation; +}) => Promise<{ connectionDetails: ConnectionDetails; runId: string }>; + interface RunProviderProps { children: React.ReactNode; - generateConnectionDetailsImpl: (params: { - graph: GraphEditorRepresentation; - }) => Promise; + generateConnectionDetailsImpl: GenerateConnectionDetails; } export function RunProvider({ @@ -55,11 +58,10 @@ function Inner({ children, }: { children?: React.ReactNode; - generateConnectionDetailsImpl: (params: { - graph: GraphEditorRepresentation; - }) => Promise; + generateConnectionDetailsImpl: GenerateConnectionDetails; }) { const { connect, disconnect, connectionState } = useEngine(); + const [runId, setRunId] = useState(null); const [starting, setStarting] = useState(false); const startingRef = useRef(false); @@ -75,7 +77,8 @@ function Inner({ const res = await generateConnectionDetailsImpl({ graph: params.graph, }); - await connect(res); + setRunId(res.runId); + await connect(res.connectionDetails); } catch (e) { console.error("Failed to start run:", e); toast.error("Failed to start run. Please try again."); @@ -88,6 +91,7 @@ function Inner({ const stopRun = useCallback(async () => { await disconnect(); + setRunId(null); }, [disconnect]); const resolvedConnectionState: ConnectionState = useMemo(() => { @@ -101,6 +105,7 @@ function Inner({ { +}): Promise<{ connectionDetails: ConnectionDetails; runId: string }> { + const run_id = v4(); const resp = await axios.post(`${getBaseUrl()}/app/run`, { type: "create_app_run", graph, - run_id: v4(), + run_id, }); - return resp.data; + + return { + connectionDetails: resp.data.connection_details, + runId: run_id, + }; } export async function createDebugConnection({ diff --git a/mcp_proxy_client/pyproject.toml b/mcp_proxy_client/pyproject.toml new file mode 100644 index 00000000..bbeb6948 --- /dev/null +++ b/mcp_proxy_client/pyproject.toml @@ -0,0 +1,64 @@ +[project] +name = "gabber-mcp-proxy-client" +version = "0.1.0" +description = "Proxy client for locally running MCP servers to interact with a remotely running Gabber Engine" +readme = "README.md" +requires-python = "==3.12.2" + +# Core dependencies needed by both modes +dependencies = [ + "aiohttp>=3.12.15", + "click>=8.2.1", + "gabber-sdk", + "mcp>=1.13.1", + "pydantic>=2.11.7", +] + +[dependency-groups] +dev = ["ruff>=0.12.5", "watchfiles>=1.1.0"] + +[tool.ruff] +# Set the target Python version (modern Python, aligns with projects like FastAPI and Pandas) +target-version = "py312" + +# Set line length to match Black's default (88 characters), common in many projects +line-length = 88 +indent-width = 4 + +# Include commonly ignored directories to avoid linting irrelevant files +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +src = ["src"] + +# Enable a broad set of lint rules for comprehensive checks +[tool.ruff.lint] +fixable = ["ALL"] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# Use Google-style docstring conventions (common in projects like Pandas) +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.uv.sources] +gabber-sdk = { path = "../sdks/python", editable = true } diff --git a/mcp_proxy_client/src/__pycache__/proxy.cpython-312.pyc b/mcp_proxy_client/src/__pycache__/proxy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bec956603cec4d4b5bf6acf530eaf8a48368c912 GIT binary patch literal 2118 zcmahKOKclObY_2E$BC0VwV@#@n+DWu)x=d>+F(T>5(=tPLQoM!zN{SY#9ep24zt@* zhg1qQLa9`#;zC*wD&kbsRLYTK(+fykY^9Z>p@&KcL2p)#MAQ>+c5TOmB1YQxo%iO= zoA=)S+||{AV10G-?pawv=vU4(7x4b7e+Os{_G4SO9|7qKB8cBO zEtr^yra+`w$rP8Qv}{UCD6N<>3C#+o0ur?p(p4sd^NS_cn{5SR%r%Z<3O(-_mYHp2 z0%57*T0HzQ>Z8QvwE;7j__N>qN8(n^srfE-;$+N`*Njk)`lex^y z457(Fc8*{8V#&zb7ID0!OS4H|FfvzSs$p1;@VM{6uy*0rqx7wA05W z!Q=1cuY7lt+Qz21AbgF!X~lq17-0*T+iUOW1oga!w;rjft<6Mg ztEQl?!KdyC2=223$?UxeHf1)me!BF0`$EhJbqNVrz@O3 zggFE)z@r>OqWA#FHpVEp4Z4W;NBt;(736W5zn;}KCSJh*tSo=f%=b;S2A}31p#?L> zOli76p9g<*4*;O~3(*$=xI73=^Pj{YKl57fB>z3J_Hr}+vsgr}v>sZEuZ7CHM<1xK zQeNUydYI1}CsQDX!8#11P&CimobEJ?kIrZ8KtpBN%S6|sln)`wT~nS0Ka}s0?<s-oTs>b<`I=D`~WAE5nzDLpv#1Of05`5W1^&YJ)L literal 0 HcmV?d00001 diff --git a/mcp_proxy_client/src/app.py b/mcp_proxy_client/src/app.py new file mode 100644 index 00000000..efad3b31 --- /dev/null +++ b/mcp_proxy_client/src/app.py @@ -0,0 +1,31 @@ +# Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved. +# SPDX-License-Identifier: SUL-1.0 + +import asyncio +import logging + +from gabber import ConnectionState, Engine + +from connection import ConnectionProvider + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class App: + def __init__(self, *, connection_provider: ConnectionProvider, run_id: str): + self.run_id = run_id + self.engine = Engine(on_connection_state_change=self.on_connection_state_change) + self.connection_provider = connection_provider + + async def run(self): + dets = await self.connection_provider.get_connection(run_id=self.run_id) + + await self.engine.connect(connection_details=dets) + mcp_servers = await self.engine.list_mcp_servers() + logger.info(f"NEIL MCP Servers: {mcp_servers}") + while True: + await asyncio.sleep(1) + + def on_connection_state_change(self, state: ConnectionState): + logger.info(f"Connection state changed: {state}") diff --git a/mcp_proxy_client/src/connection/__init__.py b/mcp_proxy_client/src/connection/__init__.py new file mode 100644 index 00000000..f3218c6d --- /dev/null +++ b/mcp_proxy_client/src/connection/__init__.py @@ -0,0 +1,4 @@ +from .connection_provider import ConnectionProvider +from .local_connection_provider import LocalConnectionProvider + +__all__ = ["ConnectionProvider", "LocalConnectionProvider"] diff --git a/mcp_proxy_client/src/connection/__pycache__/__init__.cpython-312.pyc b/mcp_proxy_client/src/connection/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbf0639492dc44b1248e40f88d4e6cd5a1eaeb06 GIT binary patch literal 334 zcmX@j%ge<81ep~FGByC|#~=<2FhLogRe+4?3@HpLj5!Rsj8Tk?AU0DDQ!aB9Gmy=k z!jjGu#Zt+t$@Y>Fs8o~jmXLFPUS4W)NoIatKv8~KW=d)iP*mI}KRGc6Q>us=sH=zt zNcd^8-V#oR>y0mfY6h#gC7A=#ACFBQr1=(me0*X~PJH}IhR;AQ!!H~CjQreG{k+u7 z9R2jfq@>g${oLdNkf9Zo@yR)vsd**(#YM^bB}IvO#Rd6ACHnF4nR%Hd@$q^EmA^P_ wa`RJ4b5iY!xPgX&JXS0QBt9@RGBVy}Fuu!R_kdgef}+(0F6&12B2J(b0Noy7=Kufz literal 0 HcmV?d00001 diff --git a/mcp_proxy_client/src/connection/__pycache__/connection_provider.cpython-312.pyc b/mcp_proxy_client/src/connection/__pycache__/connection_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..298d30543259e7eb3b1d859bdc13a0fb82759d33 GIT binary patch literal 698 zcmZWmO=}cE5Url>nXECppeurSi73KcGPj&W&;*v?xOkpRGuz$C4$Q~eGoy|tG5HVi zCm4eN!JCI%f~U1XJb2NYunL-!)jPWhVne;EqUu$>?mp>uR{_t5`u+F>{a2eTkGlwF zn;0B|1W8iJNJNN@oz%%_L^BpK0uBs7(gTo8v18iuBDZmU@4HQWVyN4dKU*RtaUC-NHR zyXNY}t|z*9mPn8c)Pru6uJt;`TcFzT-3&- zF}*!i@r&BaTmNg{__xKgGA8D_5?Muz^1>2RLMnd026a-J2kPHnu# z`O`X1+lsdRhT_v}`&YVW;WEMu4RqJS$`b4sls6F$VCwd#EPN|YS$MiR_{hQ;1@GD? z7V7@8d+2+{Yae7j+t7F<=KHiA)Q+{QT8dqD*16ZpysxigYCojBf$)P8LO#QtFL3*( P=a9$bP5&2IKd1f$72vPF literal 0 HcmV?d00001 diff --git a/mcp_proxy_client/src/connection/__pycache__/local_connection_provider.cpython-312.pyc b/mcp_proxy_client/src/connection/__pycache__/local_connection_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb73d957a4778c27805de1e7b3c85a0434dc763c GIT binary patch literal 1702 zcmZ`(UuYaf82@HxcK6cUCB0lENlk*qw&eViOH61=5RtoR9@HQdgdUV-d%I08?CoB6 zH>MY_AWaY-MD(SHv``QqB2k}xu+Ur|eQEHaVQo?H#lD?hB_a6WH?zCuTJ0?J?Qdp& z-|zco=G*--l{y00uCCo)(j|c3_#kd_1lqF*8eo7yEU1toAz(-bwWJCyQh}MpicEre zFysv|6q9Vx(6=bpSXO;VaFiuMU%|0$v*ec@`(4+0zigQ9L(apc^x@K+>FZ_7TPTGR zp=HsaJ&d3MCKQPQMadv0H6)W=WCq<}BAOfwyzP{9>rgO`6)5hCt(6UYoBoPxmv~yd z@pl6II`;OLIC=}G8sNw4u#89ZWZQ}+rePgZ`2aV3O={4OWErktT6JPbH>4}HE-lB$ z*dbuhI{k)iMXDVNG*V!7wybtIL@P0N-nwNrvYkF13_RN48y03c*@}Cp! z;GNu(Q!#V4S+;VE`h^Ro+nJ>ZnDg9H&UbYiHC#X3SI6%8aft6~yjlx-7EQlnZSB-k zzKik4dA#ntU(oyx`yayn%&~i!v7OA=Pnj3?KPxsMCG1EY=l z=k?vAuiy4s6b4VUK^pAY1D4`!ZhuV=?T(zhdH##@*R^{CFMY=D4~*>$ytz4jcVcem z#WxV{b#Y#&&z{Vqg)#pWG51@HllEH*48FYIQp16u?eB@D(Ekq+^Pp28lnHHHi2dz- zhkLv_ROltwNxnF{(MH>?kD+FV*8mi$ET8f{uPq%O>9pw zj%SkS*rX(%R+>!VxSQa7GewZ!>=igo_lIVef+Lz%af}s<&^LUj3G z!0koT#t!ZvH~fNy;iczs#s;(`Rh}oCr55n_-P7}r1aL=VE=JCDd=zn=x%@{TC}FjR zA&pjX A ConnectionDetails: ... diff --git a/mcp_proxy_client/src/connection/local_connection_provider.py b/mcp_proxy_client/src/connection/local_connection_provider.py new file mode 100644 index 00000000..cae53f9a --- /dev/null +++ b/mcp_proxy_client/src/connection/local_connection_provider.py @@ -0,0 +1,14 @@ +from .connection_provider import ConnectionProvider +from gabber import ConnectionDetails +import aiohttp + + +class LocalConnectionProvider(ConnectionProvider): + async def get_connection(self, *, run_id: str) -> ConnectionDetails: + async with aiohttp.ClientSession() as session: + req = {"type": "mcp_proxy_connection", "run_id": run_id} + async with session.post( + "http://localhost:8001/app/mcp_proxy_connection", json=req + ) as response: + data = await response.json() + return ConnectionDetails(**data["connection_details"]) diff --git a/mcp_proxy_client/src/main.py b/mcp_proxy_client/src/main.py new file mode 100644 index 00000000..610018a4 --- /dev/null +++ b/mcp_proxy_client/src/main.py @@ -0,0 +1,41 @@ +# Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved. +# SPDX-License-Identifier: SUL-1.0 + +import asyncio +import logging +from typing import Literal + +import click + +from app import App +from connection import LocalConnectionProvider, ConnectionProvider + + +@click.group() +def main_cli(): + logging.basicConfig(level=logging.INFO) + + +@main_cli.command("start") +@click.option("--backend", type=click.Choice(["local", "cloud"]), default="local") +@click.argument("run_id") +def start(backend: Literal["local", "cloud"], run_id: str): + async def run_server(): + """Start the graph editor server""" + conn_provider: ConnectionProvider + if backend == "local": + conn_provider = LocalConnectionProvider() + elif backend == "cloud": + raise NotImplementedError("Cloud backend coming soon!") + + p = App(connection_provider=conn_provider, run_id=run_id) + try: + await p.run() + except Exception as e: + logging.error(f"Error occurred while running server: {e}", exc_info=True) + + asyncio.run(run_server()) + + +if __name__ == "__main__": + main_cli() diff --git a/mcp_proxy_client/src/mcp_proxy/__init__.py b/mcp_proxy_client/src/mcp_proxy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py b/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py new file mode 100644 index 00000000..d36c0815 --- /dev/null +++ b/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py @@ -0,0 +1,57 @@ +# Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved. +# SPDX-License-Identifier: SUL-1.0 + +import json +import asyncio +import logging +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager + +import anyio +import mcp.types as types +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from livekit import rtc +from mcp.shared.message import SessionMessage +from pydantic import ValidationError + +logger = logging.getLogger(__name__) + + +async def datachannel_client_proxy( + room: rtc.Room, + other_read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], + other_write_stream: MemoryObjectSendStream[SessionMessage], +) -> rtc.Room: + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] + read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] + write_stream: MemoryObjectSendStream[SessionMessage] + write_stream_reader: MemoryObjectReceiveStream[SessionMessage] + + read_stream_writer, read_stream = anyio.create_memory_object_stream(0) + write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + + def on_message(packet: rtc.DataPacket): + if packet.topic != "__mcp__": + return + logger.debug(f"Received data packet: {packet.data}") + json_msg = types.JSONRPCMessage.model_validate_json(packet.data) + sm = SessionMessage(json_msg) + other_write_stream.send_nowait(sm) + + async def read_loop(): + async with other_read_stream: + async for session_message in other_read_stream: + if isinstance(session_message, Exception): + logger.error(f"Error in received message: {session_message}") + continue + msg_dict = session_message.message.model_dump( + by_alias=True, mode="json", exclude_none=True + ) + await room.local_participant.publish_data( + json.dumps(msg_dict), topic="__mcp__" + ) + + room.on("data_received", on_message) + await read_loop() + room.off("data_received", on_message) + return room diff --git a/mcp_proxy_client/src/mcp_proxy/sse_proxy.py b/mcp_proxy_client/src/mcp_proxy/sse_proxy.py new file mode 100644 index 00000000..71444c4b --- /dev/null +++ b/mcp_proxy_client/src/mcp_proxy/sse_proxy.py @@ -0,0 +1,12 @@ +from gabber import MCPServer, MCPTransportDatachannelProxy, MCPTransportSSE + +import mcp + + +class SSEProxy: + def __init__(self, *, server: MCPServer): + self.server = server + + async def start(self): + client_session = mcp.ClientSession() + pass diff --git a/mcp_proxy_client/uv.lock b/mcp_proxy_client/uv.lock new file mode 100644 index 00000000..68aa0169 --- /dev/null +++ b/mcp_proxy_client/uv.lock @@ -0,0 +1,707 @@ +version = 1 +revision = 2 +requires-python = "==3.12.2" + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload_time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload_time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload_time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload_time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload_time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload_time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload_time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload_time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload_time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload_time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload_time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload_time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload_time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload_time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload_time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload_time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload_time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload_time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload_time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload_time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload_time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload_time = "2025-07-29T05:51:17.239Z" }, +] + +[[package]] +name = "aiohttp-retry" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/61/ebda4d8e3d8cfa1fd3db0fb428db2dd7461d5742cea35178277ad180b033/aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1", size = 13608, upload_time = "2024-11-06T10:44:54.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/99/84ba7273339d0f3dfa57901b846489d2e5c2cd731470167757f1935fffbd/aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54", size = 9981, upload_time = "2024-11-06T10:44:52.917Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload_time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload_time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload_time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload_time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload_time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload_time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload_time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload_time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload_time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload_time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload_time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload_time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload_time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload_time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload_time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload_time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload_time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload_time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload_time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload_time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload_time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload_time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload_time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload_time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload_time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload_time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload_time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload_time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload_time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload_time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "gabber-mcp-proxy-client" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "gabber-sdk" }, + { name = "mcp" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, + { name = "watchfiles" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.12.15" }, + { name = "click", specifier = ">=8.2.1" }, + { name = "gabber-sdk", editable = "../sdks/python" }, + { name = "mcp", specifier = ">=1.13.1" }, + { name = "pydantic", specifier = ">=2.11.7" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "ruff", specifier = ">=0.12.5" }, + { name = "watchfiles", specifier = ">=1.1.0" }, +] + +[[package]] +name = "gabber-sdk" +version = "0.2.0" +source = { editable = "../sdks/python" } +dependencies = [ + { name = "aiohttp" }, + { name = "aiohttp-retry" }, + { name = "livekit" }, + { name = "numpy" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = "~=3.0" }, + { name = "aiohttp-retry" }, + { name = "livekit", specifier = "~=1.0" }, + { name = "numpy", specifier = "~=2.3.2" }, + { name = "pydantic", specifier = "~=2.11" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload_time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload_time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload_time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload_time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload_time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload_time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload_time = "2025-06-24T13:21:05.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload_time = "2025-06-24T13:21:04.772Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload_time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload_time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload_time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload_time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "livekit" +version = "1.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "types-protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/34/955ec1c81440f5c7767b916f7827be64b3d203bc1bb928f39f302d4fac6c/livekit-1.0.12.tar.gz", hash = "sha256:f81ce31d12a5f01ac3e248317d9452896fa300da677650166410a6c40c17d779", size = 311181, upload_time = "2025-07-19T17:29:18.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/ce/e89422a9b35832f38b71b3b52b751bc336cf0a51bda8e149eca0437ca38d/livekit-1.0.12-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:aba4f1bc27065ed273a53128af2477372d3f20c5cb272f10bb221425ab932075", size = 10824864, upload_time = "2025-07-19T17:29:07.28Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/18e17049d02eb3e5ed6c0c83ca85045517767749c174f96beef29ef95824/livekit-1.0.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c68c94d8e40ac481ff54956a88ce6632c910b9529d11195f30785c7392446dc3", size = 9531154, upload_time = "2025-07-19T17:29:10.045Z" }, + { url = "https://files.pythonhosted.org/packages/4d/71/d474d8cf298a2756b39ad2349d2f215fd3502d06becd63181e6c6cd62ad7/livekit-1.0.12-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c0c937489db946c2fc29ca76f165d946b65411ed73b57e12d9a83934980f5611", size = 10571762, upload_time = "2025-07-19T17:29:12.27Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fb/801a5fcb76d29c14f98101194dee22e8ed627ee1072eb4f3dd023070fbd2/livekit-1.0.12-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:34b8aeaba605044dbd5ec6599ed26a342fff20d8a61620fa7f1b03e26b485709", size = 12069549, upload_time = "2025-07-19T17:29:14.496Z" }, + { url = "https://files.pythonhosted.org/packages/d7/16/a75080702434b8e3cecdd5dbb6d681651b2bca54cb6506e2e3dc89eba858/livekit-1.0.12-py3-none-win_amd64.whl", hash = "sha256:2d75d668ee1ebdebbef6d6a349c6bc44c18c2543f22cbba2a1bdb6f3d125da18", size = 11431532, upload_time = "2025-07-19T17:29:16.916Z" }, +] + +[[package]] +name = "mcp" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/3c/82c400c2d50afdac4fbefb5b4031fd327e2ad1f23ccef8eee13c5909aa48/mcp-1.13.1.tar.gz", hash = "sha256:165306a8fd7991dc80334edd2de07798175a56461043b7ae907b279794a834c5", size = 438198, upload_time = "2025-08-22T09:22:16.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/3f/d085c7f49ade6d273b185d61ec9405e672b6433f710ea64a90135a8dd445/mcp-1.13.1-py3-none-any.whl", hash = "sha256:c314e7c8bd477a23ba3ef472ee5a32880316c42d03e06dcfa31a1cc7a73b65df", size = 161494, upload_time = "2025-08-22T09:22:14.705Z" }, +] + +[[package]] +name = "multidict" +version = "6.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload_time = "2025-08-11T12:08:48.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload_time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload_time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload_time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload_time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload_time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload_time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload_time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload_time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload_time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload_time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload_time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload_time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload_time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload_time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload_time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload_time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload_time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload_time = "2025-08-11T12:07:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload_time = "2025-08-11T12:08:46.891Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload_time = "2025-09-09T16:54:12.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload_time = "2025-09-09T15:56:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload_time = "2025-09-09T15:56:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload_time = "2025-09-09T15:56:34.175Z" }, + { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload_time = "2025-09-09T15:56:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload_time = "2025-09-09T15:56:40.548Z" }, + { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload_time = "2025-09-09T15:56:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload_time = "2025-09-09T15:56:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload_time = "2025-09-09T15:56:49.844Z" }, + { url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", size = 6320800, upload_time = "2025-09-09T15:56:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", size = 12786615, upload_time = "2025-09-09T15:56:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", size = 10195936, upload_time = "2025-09-09T15:56:56.541Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload_time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload_time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload_time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload_time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload_time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload_time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload_time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload_time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload_time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload_time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload_time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload_time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload_time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload_time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload_time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload_time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload_time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload_time = "2025-06-09T22:56:04.484Z" }, +] + +[[package]] +name = "protobuf" +version = "6.32.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/df/fb4a8eeea482eca989b51cffd274aac2ee24e825f0bf3cbce5281fa1567b/protobuf-6.32.0.tar.gz", hash = "sha256:a81439049127067fc49ec1d36e25c6ee1d1a2b7be930675f919258d03c04e7d2", size = 440614, upload_time = "2025-08-14T21:21:25.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/18/df8c87da2e47f4f1dcc5153a81cd6bca4e429803f4069a299e236e4dd510/protobuf-6.32.0-cp310-abi3-win32.whl", hash = "sha256:84f9e3c1ff6fb0308dbacb0950d8aa90694b0d0ee68e75719cb044b7078fe741", size = 424409, upload_time = "2025-08-14T21:21:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/e1/59/0a820b7310f8139bd8d5a9388e6a38e1786d179d6f33998448609296c229/protobuf-6.32.0-cp310-abi3-win_amd64.whl", hash = "sha256:a8bdbb2f009cfc22a36d031f22a625a38b615b5e19e558a7b756b3279723e68e", size = 435735, upload_time = "2025-08-14T21:21:15.046Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5b/0d421533c59c789e9c9894683efac582c06246bf24bb26b753b149bd88e4/protobuf-6.32.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d52691e5bee6c860fff9a1c86ad26a13afbeb4b168cd4445c922b7e2cf85aaf0", size = 426449, upload_time = "2025-08-14T21:21:16.687Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7b/607764ebe6c7a23dcee06e054fd1de3d5841b7648a90fd6def9a3bb58c5e/protobuf-6.32.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:501fe6372fd1c8ea2a30b4d9be8f87955a64d6be9c88a973996cef5ef6f0abf1", size = 322869, upload_time = "2025-08-14T21:21:18.282Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/2e730bd1c25392fc32e3268e02446f0d77cb51a2c3a8486b1798e34d5805/protobuf-6.32.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:75a2aab2bd1aeb1f5dc7c5f33bcb11d82ea8c055c9becbb41c26a8c43fd7092c", size = 322009, upload_time = "2025-08-14T21:21:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f2/80ffc4677aac1bc3519b26bc7f7f5de7fce0ee2f7e36e59e27d8beb32dd1/protobuf-6.32.0-py3-none-any.whl", hash = "sha256:ba377e5b67b908c8f3072a57b63e2c6a4cbd18aea4ed98d2584350dbf46f2783", size = 169287, upload_time = "2025-08-14T21:21:23.515Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload_time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload_time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload_time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload_time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload_time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload_time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload_time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload_time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload_time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload_time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload_time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload_time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload_time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload_time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload_time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload_time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload_time = "2025-04-23T18:31:51.609Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload_time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload_time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload_time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload_time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload_time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload_time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload_time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload_time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload_time = "2025-07-14T20:13:24.682Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload_time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload_time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload_time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload_time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload_time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload_time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload_time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload_time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload_time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload_time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload_time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload_time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload_time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload_time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload_time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload_time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload_time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload_time = "2025-08-27T12:13:29.71Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload_time = "2025-09-04T16:50:18.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload_time = "2025-09-04T16:49:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload_time = "2025-09-04T16:49:23.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload_time = "2025-09-04T16:49:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload_time = "2025-09-04T16:49:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload_time = "2025-09-04T16:49:32.07Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload_time = "2025-09-04T16:49:35.148Z" }, + { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload_time = "2025-09-04T16:49:38.892Z" }, + { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload_time = "2025-09-04T16:49:42.732Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload_time = "2025-09-04T16:49:46.434Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload_time = "2025-09-04T16:49:49.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload_time = "2025-09-04T16:49:53.465Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload_time = "2025-09-04T16:49:56.882Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload_time = "2025-09-04T16:49:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload_time = "2025-09-04T16:50:02.591Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload_time = "2025-09-04T16:50:05.8Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload_time = "2025-09-04T16:50:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload_time = "2025-09-04T16:50:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload_time = "2025-09-04T16:50:15.737Z" }, +] + +[[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, upload_time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload_time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload_time = "2025-07-27T09:07:44.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload_time = "2025-07-27T09:07:43.268Z" }, +] + +[[package]] +name = "starlette" +version = "0.47.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload_time = "2025-08-24T13:36:42.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload_time = "2025-08-24T13:36:40.887Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.30.2.20250822" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/68/0c7144be5c6dc16538e79458839fc914ea494481c7e64566de4ecc0c3682/types_protobuf-6.30.2.20250822.tar.gz", hash = "sha256:faacbbe87bd8cba4472361c0bd86f49296bd36f7761e25d8ada4f64767c1bde9", size = 62379, upload_time = "2025-08-22T03:01:56.572Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/64/b926a6355993f712d7828772e42b9ae942f2d306d25072329805c374e729/types_protobuf-6.30.2.20250822-py3-none-any.whl", hash = "sha256:5584c39f7e36104b5f8bdfd31815fa1d5b7b3455a79ddddc097b62320f4b1841", size = 76523, upload_time = "2025-08-22T03:01:55.157Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload_time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload_time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload_time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload_time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload_time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload_time = "2025-06-28T16:15:44.816Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload_time = "2025-06-15T19:06:59.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload_time = "2025-06-15T19:05:24.516Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload_time = "2025-06-15T19:05:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload_time = "2025-06-15T19:05:26.494Z" }, + { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload_time = "2025-06-15T19:05:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload_time = "2025-06-15T19:05:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload_time = "2025-06-15T19:05:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload_time = "2025-06-15T19:05:31.172Z" }, + { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload_time = "2025-06-15T19:05:32.299Z" }, + { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload_time = "2025-06-15T19:05:33.415Z" }, + { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload_time = "2025-06-15T19:05:34.534Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload_time = "2025-06-15T19:05:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload_time = "2025-06-15T19:05:36.559Z" }, + { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload_time = "2025-06-15T19:05:37.5Z" }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload_time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload_time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload_time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload_time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload_time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload_time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload_time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload_time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload_time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload_time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload_time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload_time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload_time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload_time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload_time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload_time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload_time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload_time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload_time = "2025-06-10T00:46:07.521Z" }, +] diff --git a/sdks/python/gabber/__init__.py b/sdks/python/gabber/__init__.py index 08730d24..6071982f 100644 --- a/sdks/python/gabber/__init__.py +++ b/sdks/python/gabber/__init__.py @@ -17,6 +17,9 @@ PadValueInteger, PadValueTrigger, PadValueVideoClip, + MCPServer, + MCPTransportDatachannelProxy, + MCPTransportSSE, ) from .media import ( AudioFrame, @@ -42,6 +45,9 @@ "PadValueInteger", "PadValueTrigger", "PadValueVideoClip", + "MCPServer", + "MCPTransportDatachannelProxy", + "MCPTransportSSE", "AudioFrame", "VideoFrame", "VideoFormat", diff --git a/sdks/python/gabber/engine/engine.py b/sdks/python/gabber/engine/engine.py index b5807daf..d65770eb 100644 --- a/sdks/python/gabber/engine/engine.py +++ b/sdks/python/gabber/engine/engine.py @@ -42,14 +42,13 @@ def __init__( Callable[[types.ConnectionState], None] ] = None, ): - logging.debug("Creating new Engine instance") - self._livekit_room: rtc.Room = rtc.Room() + self._livekit_room = rtc.Room() + self.setup_room_event_listeners() self._on_connection_state_change = on_connection_state_change self._last_emitted_connection_state: types.ConnectionState = "disconnected" self._runtime_request_id_counter: int = 1 - self._pending_requests: Dict[str, Dict[str, Callable]] = {} + self._pending_futs: Dict[str, Dict[str, Callable]] = {} self._pad_value_handlers: Dict[str, List[Callable[[Any], None]]] = {} - self.setup_room_event_listeners() @property def connection_state(self) -> types.ConnectionState: @@ -82,6 +81,7 @@ async def connect(self, *, connection_details: types.ConnectionDetails) -> None: await self._livekit_room.connect( connection_details.url, connection_details.token ) + logging.info("NEIL connected to room") while True: if self.connection_state == "connected": @@ -129,28 +129,43 @@ async def subscribe_to_node(self, *, output_or_publish_node: str): async def list_mcp_servers(self) -> List[runtime.MCPServer]: payload = runtime.RuntimeRequestPayloadListMCPServers(type="list_mcp_servers") - response = await self.runtime_request(payload) - if response.type != "list_mcp_servers": - raise ValueError("Unexpected response type") - return response.servers + logging.info("NEIL Sending list_mcp_servers request") + retries = 3 + last_exception = None + for attempt in range(retries): + try: + response = await self.runtime_request(payload) + if response.type == "list_mcp_servers": + logging.info(f"NEIL list_mcp_servers response: {response}") + return response.servers + logging.warning(f"NEIL list_mcp_servers attempt {attempt + 1} failed") + except Exception as e: + last_exception = e + + if last_exception: + raise last_exception + raise ValueError("Unexpected response type") async def runtime_request( - self, payload: types.RuntimeRequestPayload + self, payload: types.RuntimeRequestPayload, timeout: float = 2.0 ) -> types.RuntimeResponsePayload: topic = "runtime_api" request_id = str(self._runtime_request_id_counter) self._runtime_request_id_counter += 1 req = runtime.RuntimeRequest(req_id=request_id, payload=payload, type="request") - loop = asyncio.get_running_loop() - future: asyncio.Future[types.RuntimeResponsePayload] = loop.create_future() - self._pending_requests[request_id] = { - "res": lambda response: future.set_result(response), - "rej": lambda error: future.set_exception(ValueError(error)), - } - await self._livekit_room.local_participant.publish_data( - req.model_dump_json(), topic=topic, destination_identities=["gabber-engine"] - ) - return await future + future = asyncio.Future[types.RuntimeResponsePayload]() + self._pending_futs[request_id] = future + try: + await self._livekit_room.local_participant.publish_data( + req.model_dump_json(), + topic=topic, + destination_identities=["gabber-engine"], + ) + logging.debug(f"NEIL Sent runtime request: {req}") + return await asyncio.wait_for(future, timeout=timeout) + except Exception as e: + future.set_exception(e) + raise def get_source_pad(self, node_id: str, pad_id: str) -> "SourcePad": return SourcePad( @@ -168,26 +183,19 @@ def get_property_pad(self, node_id: str, pad_id: str) -> "PropertyPad": ) def setup_room_event_listeners(self) -> None: - def on_connected(): - self._emit_connection_state_change() - - self._livekit_room.on("connected", on_connected) - - def on_disconnected(reason: Any): + def on_connected(state: rtc.ConnectionState): + logging.info(f"NEIL on connected!!!!!!!!!!!: {state}") self._emit_connection_state_change() - self._livekit_room.on("disconnected", on_disconnected) - def on_participant_connected(participant: rtc.RemoteParticipant): self._emit_connection_state_change() - self._livekit_room.on("participant_connected", on_participant_connected) - def on_participant_disconnected(participant: rtc.RemoteParticipant): self._emit_connection_state_change() + self._livekit_room.on("connection_state_changed", on_connected) + self._livekit_room.on("participant_connected", on_participant_connected) self._livekit_room.on("participant_disconnected", on_participant_disconnected) - self._livekit_room.on("data_received", self._on_data) def _add_pad_value_handler( @@ -229,14 +237,14 @@ def _on_data( payload = resp.payload if resp.error is not None: logging.error("Error in request: %s", msg["error"]) - pending_request = self._pending_requests.get(msg["req_id"]) + pending_request = self._pending_futs.get(msg["req_id"]) if pending_request: - pending_request["rej"](msg["error"]) + pending_request.set_exception(Exception(msg["error"])) else: - pending_request = self._pending_requests.get(msg["req_id"]) + pending_request = self._pending_futs.get(msg["req_id"]) if pending_request: - pending_request["res"](payload) - self._pending_requests.pop(msg["req_id"], None) + pending_request.set_result(payload) + self._pending_futs.pop(msg["req_id"], None) elif msg["type"] == "event": resp = runtime.RuntimeEvent.model_validate(msg) payload = resp.payload From bfa19ac55b3901f796c9b1cc229b2a90baf5114b Mon Sep 17 00:00:00 2001 From: Keepingitneil Date: Tue, 9 Sep 2025 22:20:12 -0700 Subject: [PATCH 4/9] mcp hello world working --- engine/src/core/mcp/datachannel_transport.py | 46 +--------- engine/src/core/mcp/mcp_server_config.py | 18 +++- mcp.yaml | 5 +- .../src/__pycache__/app.cpython-312.pyc | Bin 0 -> 4599 bytes mcp_proxy_client/src/app.py | 44 +++++++++- mcp_proxy_client/src/mcp_proxy/__init__.py | 3 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 229 bytes .../datachannel_transport.cpython-312.pyc | Bin 0 -> 2921 bytes .../__pycache__/mcp_proxy.cpython-312.pyc | Bin 0 -> 4788 bytes .../src/mcp_proxy/datachannel_transport.py | 18 +--- mcp_proxy_client/src/mcp_proxy/mcp_proxy.py | 81 ++++++++++++++++++ mcp_proxy_client/src/mcp_proxy/sse_proxy.py | 12 --- sdks/python/gabber/__init__.py | 2 + sdks/python/gabber/generated/runtime.py | 16 +++- 14 files changed, 166 insertions(+), 79 deletions(-) create mode 100644 mcp_proxy_client/src/__pycache__/app.cpython-312.pyc create mode 100644 mcp_proxy_client/src/mcp_proxy/__pycache__/__init__.cpython-312.pyc create mode 100644 mcp_proxy_client/src/mcp_proxy/__pycache__/datachannel_transport.cpython-312.pyc create mode 100644 mcp_proxy_client/src/mcp_proxy/__pycache__/mcp_proxy.cpython-312.pyc create mode 100644 mcp_proxy_client/src/mcp_proxy/mcp_proxy.py delete mode 100644 mcp_proxy_client/src/mcp_proxy/sse_proxy.py diff --git a/engine/src/core/mcp/datachannel_transport.py b/engine/src/core/mcp/datachannel_transport.py index ccf3385b..62b51b60 100644 --- a/engine/src/core/mcp/datachannel_transport.py +++ b/engine/src/core/mcp/datachannel_transport.py @@ -19,7 +19,7 @@ @asynccontextmanager async def datachannel_host( - room: rtc.Room, participant: str + room: rtc.Room, participant: str, mcp_name: str ) -> AsyncGenerator[ tuple[ MemoryObjectReceiveStream[SessionMessage | Exception], @@ -27,6 +27,7 @@ async def datachannel_host( ], None, ]: + topic = f"__mcp__:{mcp_name}" read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] write_stream: MemoryObjectSendStream[SessionMessage] @@ -38,7 +39,7 @@ async def datachannel_host( packet_q = asyncio.Queue[rtc.DataPacket | None]() def on_message(packet: rtc.DataPacket): - if packet.topic != "__mcp__": + if packet.topic != topic: return if not packet.participant: @@ -75,7 +76,7 @@ async def dc_writer(): by_alias=True, mode="json", exclude_none=True ) await room.local_participant.publish_data( - json.dumps(msg_dict), topic="__mcp__" + json.dumps(msg_dict), topic=topic ) room.on("data_received", on_message) @@ -91,42 +92,3 @@ async def dc_writer(): tg.cancel_scope.cancel() room.off("data_received", on_message) - - -async def datachannel_client_proxy( - url: str, - token: str, - other_read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], - other_write_stream: MemoryObjectSendStream[SessionMessage], -) -> rtc.Room: - """ - Connects to a LiveKit room and returns the Room object. - """ - room = rtc.Room() - - def on_message(packet: rtc.DataPacket): - if packet.topic != "__mcp__": - return - logger.debug(f"Received data packet: {packet.data}") - json_msg = types.JSONRPCMessage.model_validate_json(packet.data) - sm = SessionMessage(json_msg) - other_write_stream.send_nowait(sm) - - async def read_loop(): - async with other_read_stream: - async for session_message in other_read_stream: - if isinstance(session_message, Exception): - logger.error(f"Error in received message: {session_message}") - continue - msg_dict = session_message.message.model_dump( - by_alias=True, mode="json", exclude_none=True - ) - await room.local_participant.publish_data( - json.dumps(msg_dict), topic="__mcp__" - ) - - room.on("data_received", on_message) - await room.connect(url, token) - await read_loop() - room.off("data_received", on_message) - return room diff --git a/engine/src/core/mcp/mcp_server_config.py b/engine/src/core/mcp/mcp_server_config.py index da45c27b..b4b55bfa 100644 --- a/engine/src/core/mcp/mcp_server_config.py +++ b/engine/src/core/mcp/mcp_server_config.py @@ -14,12 +14,26 @@ class MCPTransportSSE(BaseModel): url: str +class MCPTransportSTDIO(BaseModel): + type: Literal["stdio"] = "stdio" + command: str + args: list[str] + + +MCPLocalTransport = Annotated[ + MCPTransportSTDIO | MCPTransportSSE, Field(discriminator="type") +] + + class MCPTransportDatachannelProxy(BaseModel): type: Literal["datachannel_proxy"] = "datachannel_proxy" - local_transport: MCPTransportSSE + local_transport: MCPLocalTransport -MCPTransport = Annotated[MCPTransportDatachannelProxy, Field(discriminator="type")] +MCPTransport = Annotated[ + MCPTransportDatachannelProxy | MCPTransportSTDIO | MCPTransportSTDIO, + Field(discriminator="type"), +] class MCPServer(BaseModel): diff --git a/mcp.yaml b/mcp.yaml index 8ba44c7d..9a7fb838 100644 --- a/mcp.yaml +++ b/mcp.yaml @@ -3,5 +3,6 @@ servers: transport: type: datachannel_proxy local_transport: - type: sse - url: http://localhost:9876/sse + type: stdio + command: uvx + args: [blender-mcp] diff --git a/mcp_proxy_client/src/__pycache__/app.cpython-312.pyc b/mcp_proxy_client/src/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bcf1e612e928dbb8b4209cd887eb562273b950be GIT binary patch literal 4599 zcmb7HYitwQ6~1>K_V^LUiDL&$0Ea})PRNUHfh3D6g+Nx2C1eX?7gv>KJd@zy*y)UU zBu!QbP!*(fDXrRowiRq!3ASX_w5qgy{3v06v{L`X8<5>;Dj`*?cK>DZN=wsL>N$5j z6JoMLz0#a}&VAo=&pF?jKUY+^36vZEy??q(BINHVSVbr?#gBlQB??h!ibTztMk&Z# ziW}vl{HPEWM#ZQ|v5!wVMx9Y7WaD;FQ(k1o~VZsj;M5GleNiOtybKlrWEzD zG27PqEDmNyEPkz?b67KR^;e1Fyh0S|9%qh-RTxQ2KEVM4GH+D&X3}Xj zkxgdO$FlLPYB&z2hm&d5a2@F#Jf>>rR86J^uVoWZRj&;m)H3Ii3hR_$B#0NrVT71c z @z+5ri%b=d~3vqUARzbL0rl~*`b7!nlziVzhQ;R=a56j5~!af$<4q;aP#8GO&! zm=Q?Whm4JtHZ~mE*>o(aBubit>l4D4zTyeC*CaPdV!)Xti@mH&k`e1vRuj~7)Y2u) zS{qA2vPX{-nHzw6hBN8dGl$Yqb+N?hczRfsMXdr>({PSf3B+)yOr@F^1W>u4tEnL- zuz@;m6^TSvTiZ`(M%DJTnoPA1$4{M7wf51(7+(0ocr1}hs_AUIt|i*zV`FV&c<9kp3f09Ml*fLtVnP~EgJ<5>>X{YJ=lzLD#E<6-DcY$-JCnHC;Nks=Hu zwgOSWLtlok_%l#zKcVnope*buDs#kI>zStDc{iMe`Y$V!bdI;?m7jy+9XHw?U$3m(E6L?+O!1Ta0rCyd{**Au zjZ~FQ194KY_NQbfC=eKhZ&g;lXnjh?$zC$^4tyu!>w?eoK7XFd0+=JSBd|(A%Vb8; zPoFVOMa{;ODP49mi!*$~YW5k+WiyRR=Dx?yj)A`?^^B(Z;Z#ONER{T`o&giXWesO( zQNy1~>e(2Yrq1GnZn)!$QW6a5d>m%PQz`HR)Pf}h@!;MQPPA-bFlOq%0`gDt6zn>Joey+;^FoVV#NedKLk;h}NGK`OWBeN8!E z(~?ilOL9(7=QuUlBy zRS*1wYTD}+9t7Hf|B$1-KH)>&fxMd{^)l*1YKSb(Budd8)-@d#U9tETAhRTkZLDnM zpbDL&?~otc4unPp@oXa8Ysc3J+ah9yGNn5rbA}_886H+OLrA8FGBU?rRcnMzbUaAu zy;!81*oN=*#i6kQCxw7qB+HWTn&+x#G1M_1oOdqPbUu>0v>NCdkX;zs((zF>7Bk$j z*l0#Mn?l|ji+%HKJZ1K17H&uSWak%mias0enD_Xt~yS(4_#RwQUwdJ~W3bJAT+H9w2h(&z!}E?j#nMP z3&1-SlNS-@0PINAg}@zkk4v&k+YHNUs3kixBg9$oRy1LB7-H5Mv`r#NSwINM3P{!| z%_r^y+!3>J{fy35Hk<$`8C_MZ=fv&SJtr!4VpCaMutDY}99|0qoKfBhkm4@XY=X*F zs0GORj+bf9aG3F7oxS3-;WHKA0`mWWOBUk;DK z!<@0+80f{-T@*V3Asx;S}dWP_iKzb|{tSK#4a2oRX{!kt2v1WknG!eV4-}gWR&w zDC%@3I&MN;M+3>)pxj8b#wk8)rvoD!K)k|D@)+F|eonCNM9E1=I;Qxy_?Z)CeV(2t z7xpydlKzO$Gdh2bvRJ|kb-LdH1u~I<-wX-chXaNkX+VQXD03skvUG|4&z1O{q+rK=& zYrbJ|>z?~H-}UDGeK~*MzX1+m#Oh=3j#d3A9OP2t{_XTmpr@7mj`r*nelGy`hfUD7 zP)ql36&5!6k(W87J1KN7w7T|(`Gq~<{XzbLUxfAtK@nOWgazR35C}`NxWVlHO%TlF zG;0UF5j)`FP_sZ(K(&^Yx50VVC!(d~NjzXW19LmcE+4q1+m|IkL!-hL0q9RmY!#OOa;!6Z(?yQ%Cdq z8{3|K7e<*kX2BgKv%n2ExHlxC5QwG#?MLjWY0V(S@@7iF->Iz&+oxx?3#JD;!2|7B zyAia3SMum#Go6-CF@NF)jzeH&-eE@9L_D2PQ>IFNy-Y}{3UjkkBsByEPIwn90d&L} zL`{Usy})eW2SfGOftaRWy9MCK3n(;u`1;`|M05qG4-~xqYyDUIXO5r;1Ph_?toyqA zQz!JHfo}l=59b4NE+C_s2mj))L1Pa7M@P@R)YMZ=e=c@$zo2^^^FkN*YaSXtpk3T= z13i`Ef|mnv!AFs=#I6C^qaiG6G7=2&8tTpvz6Qy&nWNxaXowKZy_xlBeJHjd!Q{tG zSXcsp1alRA4-hk9*?OD5BfTqOva)r>O~s@1_U0laDo5#x!zni0O%@4MPpYZdT!e<8 zxg;(m2TbKVF_lZEhYc`n{J}IcCiD>v4`(<_8NYdS_u6lSnQpo4|4|n93>W-NyvCA4 z^Xe?(c}~UkWCAkEq2#b392)35YGz>UR_kxND(y3~yRT}4uswQ~egX)*1EocY(8|9O z-#^Ii9NGOhQu{HHKPKBAyTa48c~>~+3g75lay4Fb6iDErGfx6J61dTHtL0|PBhvJ_ VqndVqN`QRcTt|aX2oR=@{{bWTzbXI# literal 0 HcmV?d00001 diff --git a/mcp_proxy_client/src/app.py b/mcp_proxy_client/src/app.py index efad3b31..1a4f6cbd 100644 --- a/mcp_proxy_client/src/app.py +++ b/mcp_proxy_client/src/app.py @@ -4,10 +4,13 @@ import asyncio import logging -from gabber import ConnectionState, Engine +from gabber import ConnectionState, Engine, MCPServer +from livekit import rtc from connection import ConnectionProvider +from mcp_proxy import MCPProxy + logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -20,12 +23,45 @@ def __init__(self, *, connection_provider: ConnectionProvider, run_id: str): async def run(self): dets = await self.connection_provider.get_connection(run_id=self.run_id) + proxy_supervisor = ProxySupervisor(room=self.engine._livekit_room) await self.engine.connect(connection_details=dets) mcp_servers = await self.engine.list_mcp_servers() - logger.info(f"NEIL MCP Servers: {mcp_servers}") - while True: - await asyncio.sleep(1) + for s in mcp_servers: + proxy_supervisor.add_server(s) + + await proxy_supervisor.wait_all() def on_connection_state_change(self, state: ConnectionState): logger.info(f"Connection state changed: {state}") + + +class ProxySupervisor: + def __init__(self, *, room: rtc.Room): + self.tasks: list[asyncio.Task] = [] + self.room = room + self._closed = False + + def add_server(self, server: MCPServer): + self.tasks.append(asyncio.create_task(self._supervise_server(server))) + + async def _supervise_server(self, server: MCPServer): + while not self._closed: + proxy = MCPProxy(room=self.room, server=server) + try: + await proxy.run() + except Exception as e: + logger.error(f"Error in MCPProxy: {e}", exc_info=True) + await asyncio.sleep(2) + + async def wait_all(self): + await asyncio.gather(*self.tasks) + + async def aclose(self): + self._closed = True + for task in self.tasks: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass diff --git a/mcp_proxy_client/src/mcp_proxy/__init__.py b/mcp_proxy_client/src/mcp_proxy/__init__.py index e69de29b..9e719d0f 100644 --- a/mcp_proxy_client/src/mcp_proxy/__init__.py +++ b/mcp_proxy_client/src/mcp_proxy/__init__.py @@ -0,0 +1,3 @@ +from .mcp_proxy import MCPProxy + +__all__ = ["MCPProxy"] diff --git a/mcp_proxy_client/src/mcp_proxy/__pycache__/__init__.cpython-312.pyc b/mcp_proxy_client/src/mcp_proxy/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2ee34edafcc0258663f568bac2636a8273e2e0b GIT binary patch literal 229 zcmX@j%ge<81joJ}$glv?k3k$5V1hC}ivbza8B!Qh7;_kM8KW2(8B&$caZxftP(MCCGcU6wK3=b&@)w6qZhlH>PO4oI gC(tmEBZ~Qf#0O?ZM#j4g@(;L0J9HY^i`ao;0MGqAy#N3J literal 0 HcmV?d00001 diff --git a/mcp_proxy_client/src/mcp_proxy/__pycache__/datachannel_transport.cpython-312.pyc b/mcp_proxy_client/src/mcp_proxy/__pycache__/datachannel_transport.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a1d5ee14a7878742d11817258cdf51fe8f0232c GIT binary patch literal 2921 zcmai0U2Gf25#GH&9{(hZltqb@9jZwa(?3#ew{|Y;E#U+IP#s*HaG%(m&1G0h?q~IKi+dYnBYs}^O zIFFf(%L#cgF6O1UWR3ZpD=)`oix+b4yeIC#h(iQtyPO}idg8l>?NQvXh(ylzwsa1s zNTgkH5s`Svo_QBSJ)lrzA`y?pxu5;jq(|{w6XG339P|E@xw;VJ%%0asUZcf}6Iqfn zE|V0=yiLXpN|JfAYqx5gsERF*VUwpu3c9v&qU#w=eGR%~nwSFBw7eaXQj2Wt?1ZR2BGkyu)t=|DzM$6nXzbuU*%r+k{;GdwM zaqmb9XMomjSkymLcY$n|P~F>cbGS0R`n5lLqjg_kQ*OKItN7MKNA6Dk zYWjoeyKk-U9omqGH-p`^;7iruOSRxoH8}L(TfZ6m_1MGU*|NOpkF1`3=>Kw=tIOe< z{9;vp@fY+@^1&_ek5Y^{jbZ&q@NWGWAbW?mW3j@06)i#fdnz)xW9YaOFM=H}i^ym~ zePI`HooJ&yMl=4O8CL-2CxLt87yz*eUT2yBS#OJXI0bTm!rd3{K?Jk5hyjj1o8L{d zP3X3yMiaK#KuhXyEr?fy`=Ya&X13s+Y;mIkdEh3z*Wq=4os|iAn)V#*A*|odE+-Jn zFGch`I*OJYjpd1HbUe#p^d?v^n>qeXG>e`55;;SuMxz-uO6^pxM0YSY7=7Io7`|h? zDNhs=5D`h;6j*$iJ~Efe%_s!IK_xM4fj;IjJsCZt>PAvc5z~EUE=39kBzw9CmTroK z{m69fcsD(EpeZx?g4tnVHc?1YBa_M$lB!|)3NsVAj6RiMK&SgG56o7NiDtWQR}m)~ zpn&M7ak#x zJMbAJ_1NGetN*^T3B9n1%+yG!%G*xDJ>FkVXegWxK(_9JU*6AlPoSqk}__co182ZILZD>;LGp zu$(2=X&JwRN5j&)Ja9kcN00Njh0&;R2aop3@A1IBe`*MXYn^y>zr40rWPBKd?OHDe z?b?3KBvFg@d71n$9vu+Yj!29@E}a%#YbWvOuy1Y91^jO@`=jY+dk;n4x;iK^fwFXI zdcFzLOID3DIZZWrZE`Z^vJaC5t$nsujt#TnAx5lh%g*~s^vacJ^cDM@yZ_hW(yxH2 z_03zr{M$Oth7YxeZq41wuKW5*XBvVe9{TC=pO3sh@&y9+i#=X3&Zz5MFcSIZYy z;~T<(KYN1j9;yj@*M+@xL0&$4^=vt@n%xj$b#HrVtRW(?>#@#4|N1F(st;hj|FTnyI4)isA~=tKWF& zexW2aq$u7~_xNufs2phUz<=7-(GZ~rXm=m|aQ{cK>d~=k=zPP)WHRa*xcmKT_ejId Th8`rjm*2Sh#^+ubS=0J2^#!|4 literal 0 HcmV?d00001 diff --git a/mcp_proxy_client/src/mcp_proxy/__pycache__/mcp_proxy.cpython-312.pyc b/mcp_proxy_client/src/mcp_proxy/__pycache__/mcp_proxy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd5dc642766ccf60f875e0d9e4074c04146bd925 GIT binary patch literal 4788 zcmb^!ZEO_Bb>?>Odwo9ci{os}*)fKTKMu#iHW)A$ppieYQw))EiF7({7yHh8x9;o` zuxnJ5pvqKKkUur3suF%B2&%xZB!3zc<)dl;+{s3J-8gEbv{LiWic=*-{n0nO-aF%h z2vSFPZ{BIXIq({eXVr|}= z^y;`>+?4kveLC(C{rNyLkPjwZpO%h1f1Jt8e8p!EjPhl+L94nrrV&_ zLzvmSo;L7XZgVEp{ygHj3y80|X)#8VTc>f%^9?0NVp#JeXDPL0}?XCFVIV$8OGCg`SCuEdkLPp5GDvT*a zNawZC`k^tQz#IH;sJlRSn6DbTwnA!(8VqArQFv7lGlnM&Sh*hifSyMJqE{km;jzHv z9P0%QoVN(pQ!LM1V3RhUy?~N--U`q$ZHw782ROj+4ND{`ui0eXkD0ZDgwsI!pMicg z31A-0;#rh3R0Eu~0A)(Ubp&ed*bq|err6Cj1V&Bk4Oy!xF^z8Ek(iD6fk6TQG`nng zn9#K~mjDNzlIe+SF5NjPB`r_J7>$($@l$j-&c(}2~M<6U#?Z);aU7FC=|33^bk!+%u)7Yrc`j#VmdoJF~c6REJ#auwH^ z9B{GC#LcheiiP}2sa%7c+Z?ah^?q)&jw3^OYt zkP!1zifphgFxy`lQY1-S8}L<_r$CoH0eWI#&6Sn21xT!gj9?^QXJrqBrf1D2O5@{D zTd~=~DM_o<_nE57&tSl97u<=v;X#i$lf8uG@k4>h9Ftt|^8OEOGtihNd?suT82A zU8RODwc${y;n3XZcQ)jWT#euAU#jgZKhynQ{7zhb=0xe46U%{y;GFM# zEbYFMH+IE+?G>2ftG_zB)P7)TQ~!oJ(dBTR8tyEGJIl>oYI9$yxo;t^KKEkjxfj1< zEL$S03>yrsGB&EwR@LZ5n;VNy9bDK}Joxis1t?!^>X3 z>TN7}8{e7yGq>bD0ai76z^Z!B+~G^Z--S?(-`I5T`+*PZKB!ao43zc^6nh7Y;h`mN zxa;TM$7flH;1nc ztM$7}^}84Ad(=?R-1C(=@!RZs?mKRE*TK@RgT;7a!ME`I!k%LI=n^+ljx@a4cCAf~ zbe1BWi;*rh&^33YGPC_1{@uwNlWNC-QpbT}d;j}nVf(`7Vt8PQ8~lfPcx~?-fA{4Z zFRShQOYQrMZ3l|s{(IcPm7^GOLwJRQXLTDyNO@(b*JqJfs5EE4hd8bCu%1x`L@2BPY%851CcT;fztwVTk55o?lA9-|^J? zjIO_*tGX%Hj0n7mUxFED&{^gsbOzH%PiXZAVL*>jwsc06;E_XXI?a}$QBTzDddSL3 zn&*(t5CvY3Ju$4u4w)9_MqD7f0iB`mNaICdXd48kECSF&L)qiIEM1gTPjkuByy$6B z9W6yiOW9p}x&Jr)WzKtf^x~+>HI=xgC9ZkJ!qf!Ip~$@ShNOD8m%Q6o?1-ydVUTmj z4=WC|spbB%Kcf0$C4cN&glmF-^9EKJ=>H=hfdyY{Nc7{kaH5I5{xo2Bx!Obvx?6`6 z&FtNJmf}%{@|!WmTl|TA%-uZ}%IU)t-$zGBVov(pp)oa;(p;%jUgA%S6z5W@Uz|>h z#t5OW3XMGm??gh6j?fyIbWuRPas!dA6sO^n_)I|kEN=&J0j*f9)@R>h-gaDftRO&d z$5v_gt!IZdfme@VyESNNpp#<_qz5|9PLmI0NX@mDP{iy><3(sq7pAikiBTz=@xU|8 zqDhPBS&3dj&;Sqd$*f|S?}7Z&VZ7=);DN8FC(XN_NmG-Nt9+C#Po#;!cjS$CK5sf1qhp_OG6%>I4A5ICp9BE+0pl-_2Yz3o?O!17YvlUgV#OBlFSh)f zLpTilFH!7E)c$vL?4PLbD`#YGv+9hLoRO=&i_Yfrwleacx2wosLjJ2eXav27c6@K^ M#lde80NsZF0zSzN-v9sr literal 0 HcmV?d00001 diff --git a/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py b/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py index d36c0815..52107019 100644 --- a/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py +++ b/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py @@ -2,36 +2,26 @@ # SPDX-License-Identifier: SUL-1.0 import json -import asyncio import logging -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager -import anyio import mcp.types as types from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from livekit import rtc from mcp.shared.message import SessionMessage -from pydantic import ValidationError logger = logging.getLogger(__name__) async def datachannel_client_proxy( room: rtc.Room, + mcp_name: str, other_read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], other_write_stream: MemoryObjectSendStream[SessionMessage], ) -> rtc.Room: - read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] - read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] - write_stream: MemoryObjectSendStream[SessionMessage] - write_stream_reader: MemoryObjectReceiveStream[SessionMessage] - - read_stream_writer, read_stream = anyio.create_memory_object_stream(0) - write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + topic = f"__mcp__:{mcp_name}" def on_message(packet: rtc.DataPacket): - if packet.topic != "__mcp__": + if packet.topic != topic: return logger.debug(f"Received data packet: {packet.data}") json_msg = types.JSONRPCMessage.model_validate_json(packet.data) @@ -48,7 +38,7 @@ async def read_loop(): by_alias=True, mode="json", exclude_none=True ) await room.local_participant.publish_data( - json.dumps(msg_dict), topic="__mcp__" + json.dumps(msg_dict), topic=topic ) room.on("data_received", on_message) diff --git a/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py b/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py new file mode 100644 index 00000000..d10540a2 --- /dev/null +++ b/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py @@ -0,0 +1,81 @@ +import asyncio +import logging +from contextlib import AsyncExitStack +import anyio + +import mcp +from gabber import ( + MCPServer, + MCPTransportDatachannelProxy, + MCPTransportSSE, + MCPTransportSTDIO, +) +from livekit import rtc +from mcp.client.sse import sse_client +from mcp.client.stdio import stdio_client +from mcp.shared.message import SessionMessage +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream + +from .datachannel_transport import datachannel_client_proxy + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class MCPProxy: + def __init__(self, *, room: rtc.Room, server: MCPServer): + self.room = room + self.server = server + self.session: mcp.ClientSession | None = None + self.exit_stack = AsyncExitStack() + self.proxy_task: asyncio.Task | None = None + + async def run(self): + if isinstance(self.server.transport, MCPTransportDatachannelProxy): + logger.info(f"Starting MCPProxy for {self.server}") + local_transport = self.server.transport.local_transport + read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] + write_stream: MemoryObjectSendStream[SessionMessage] + if isinstance(local_transport, MCPTransportSSE): + read_stream, write_stream = await self.exit_stack.enter_async_context( + sse_client("http://localhost:9876") + ) + elif isinstance(local_transport, MCPTransportSTDIO): + read_stream, write_stream = await self.exit_stack.enter_async_context( + stdio_client( + mcp.StdioServerParameters( + command=local_transport.command, args=local_transport.args + ) + ) + ) + else: + raise ValueError(f"Unsupported local transport: {local_transport}") + self.session = await self.exit_stack.enter_async_context( + mcp.ClientSession(read_stream=read_stream, write_stream=write_stream) + ) + # self.proxy_task = asyncio.create_task( + # datachannel_client_proxy( + # room=self.room, + # mcp_name=self.server.name, + # other_read_stream=read_stream, + # other_write_stream=write_stream, + # ) + # ) + logger.info(f"Initializing mcp session for: {self.server.name}") + await self.session.initialize() + logger.info(f"MCPProxy session initialized for {self.server.name}") + prompts = await self.session.list_prompts() + logger.info(f"MCPProxy prompts for {self.server.name}: {prompts}") + tools = await self.session.list_tools() + logger.info(f"MCPProxy tools for {self.server.name}: {tools}") + # await self.proxy_task + logger.info(f"MCPProxy finished {self.server.name}") + + async def aclose(self): + await self.exit_stack.aclose() + if self.proxy_task: + self.proxy_task.cancel() + try: + await self.proxy_task + except asyncio.CancelledError: + pass diff --git a/mcp_proxy_client/src/mcp_proxy/sse_proxy.py b/mcp_proxy_client/src/mcp_proxy/sse_proxy.py deleted file mode 100644 index 71444c4b..00000000 --- a/mcp_proxy_client/src/mcp_proxy/sse_proxy.py +++ /dev/null @@ -1,12 +0,0 @@ -from gabber import MCPServer, MCPTransportDatachannelProxy, MCPTransportSSE - -import mcp - - -class SSEProxy: - def __init__(self, *, server: MCPServer): - self.server = server - - async def start(self): - client_session = mcp.ClientSession() - pass diff --git a/sdks/python/gabber/__init__.py b/sdks/python/gabber/__init__.py index 6071982f..4eda30b1 100644 --- a/sdks/python/gabber/__init__.py +++ b/sdks/python/gabber/__init__.py @@ -20,6 +20,7 @@ MCPServer, MCPTransportDatachannelProxy, MCPTransportSSE, + MCPTransportSTDIO, ) from .media import ( AudioFrame, @@ -48,6 +49,7 @@ "MCPServer", "MCPTransportDatachannelProxy", "MCPTransportSSE", + "MCPTransportSTDIO", "AudioFrame", "VideoFrame", "VideoFormat", diff --git a/sdks/python/gabber/generated/runtime.py b/sdks/python/gabber/generated/runtime.py index ffa2c0d1..86dbb816 100644 --- a/sdks/python/gabber/generated/runtime.py +++ b/sdks/python/gabber/generated/runtime.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: runtime.json -# timestamp: 2025-09-08T23:21:21+00:00 +# timestamp: 2025-09-10T05:08:08+00:00 from __future__ import annotations @@ -14,6 +14,12 @@ class MCPTransportSSE(BaseModel): url: Annotated[str, Field(title='Url')] +class MCPTransportSTDIO(BaseModel): + type: Annotated[Literal['stdio'], Field(title='Type')] = 'stdio' + command: Annotated[str, Field(title='Command')] + args: Annotated[list[str], Field(title='Args')] + + class PadValueAudioClip(BaseModel): type: Annotated[Literal['audio_clip'], Field(title='Type')] = 'audio_clip' transcript: Annotated[str, Field(title='Transcript')] @@ -128,7 +134,10 @@ class MCPTransportDatachannelProxy(BaseModel): type: Annotated[Literal['datachannel_proxy'], Field(title='Type')] = ( 'datachannel_proxy' ) - local_transport: MCPTransportSSE + local_transport: Annotated[ + Union[MCPTransportSTDIO, MCPTransportSSE], + Field(discriminator='type', title='Local Transport'), + ] class RuntimeEvent(BaseModel): @@ -164,7 +173,8 @@ class RuntimeRequest(BaseModel): class MCPServer(BaseModel): name: Annotated[str, Field(title='Name')] transport: Annotated[ - MCPTransportDatachannelProxy, Field(discriminator='type', title='Transport') + Union[MCPTransportDatachannelProxy, MCPTransportSTDIO], + Field(discriminator='type', title='Transport'), ] From ddedb0db3c3b428d4b4989e224964acb91d83b12 Mon Sep 17 00:00:00 2001 From: Keepingitneil Date: Wed, 10 Sep 2025 11:51:00 -0700 Subject: [PATCH 5/9] cooking --- engine/src/core/graph/runtime_api.py | 8 -- engine/src/core/mcp/__init__.py | 2 + engine/src/core/mcp/datachannel_transport.py | 6 ++ engine/src/nodes/core/tool/mcp.py | 59 ++++++++++++++- engine/src/nodes/llm/base_llm.py | 70 ++++++++++++++---- engine/src/services/repository/repository.py | 2 +- .../datachannel_transport.cpython-312.pyc | Bin 2921 -> 2930 bytes .../__pycache__/mcp_proxy.cpython-312.pyc | Bin 4788 -> 3774 bytes .../src/mcp_proxy/datachannel_transport.py | 4 +- mcp_proxy_client/src/mcp_proxy/mcp_proxy.py | 25 ++----- 10 files changed, 131 insertions(+), 45 deletions(-) diff --git a/engine/src/core/graph/runtime_api.py b/engine/src/core/graph/runtime_api.py index 7bbcc356..14784f83 100644 --- a/engine/src/core/graph/runtime_api.py +++ b/engine/src/core/graph/runtime_api.py @@ -11,7 +11,6 @@ from core.editor import serialize from core.node import Node from nodes.core.media.publish import Publish -from nodes.core.tool import MCP PING_BYTES = "ping".encode("utf-8") @@ -82,11 +81,7 @@ def on_pad(p: pad.Pad, value: Any): p._add_update_handler(on_pad) def on_data(packet: rtc.DataPacket): - logging.info(f"NEIL **************** Received data packet: {packet.data}") if not packet.topic or packet.topic != "runtime_api": - logging.warning( - f"Unknown data packet topic: {packet.topic} - {packet.data}" - ) return try: @@ -97,9 +92,6 @@ def on_data(packet: rtc.DataPacket): req_id = request.req_id ack_resp = RuntimeRequestAck(req_id=req_id, type="ack") complete_resp = RuntimeResponse(req_id=req_id, type="complete") - logging.info( - "NEIL ---------------- Handling runtime_api request: %s", request - ) dc_queue.put_nowait( QueueItem(payload=ack_resp, participant=packet.participant) diff --git a/engine/src/core/mcp/__init__.py b/engine/src/core/mcp/__init__.py index 29ce78d8..247a6312 100644 --- a/engine/src/core/mcp/__init__.py +++ b/engine/src/core/mcp/__init__.py @@ -6,6 +6,7 @@ MCPServerConfig, ) from .mcp_server_provider import MCPServerProvider +from .datachannel_transport import datachannel_host __all__ = [ "MCPTransport", @@ -14,4 +15,5 @@ "MCPServer", "MCPServerProvider", "MCPServerConfig", + "datachannel_host", ] diff --git a/engine/src/core/mcp/datachannel_transport.py b/engine/src/core/mcp/datachannel_transport.py index 62b51b60..c1c42bb6 100644 --- a/engine/src/core/mcp/datachannel_transport.py +++ b/engine/src/core/mcp/datachannel_transport.py @@ -58,11 +58,16 @@ async def on_message_loop(): try: message = types.JSONRPCMessage.model_validate_json(packet.data) + logging.info(f"NEIL message received on topic {message}") session_message = SessionMessage(message) await read_stream_writer.send(session_message) except ValidationError as exc: + logging.error(f"NEIL DC message validation error: {exc}") # If JSON parse or model validation fails, send the exception await read_stream_writer.send(exc) + except Exception as exc: + logging.error(f"NEIL DC unexpected error: {exc}") + await read_stream_writer.send(exc) async def dc_writer(): """ @@ -75,6 +80,7 @@ async def dc_writer(): msg_dict = session_message.message.model_dump( by_alias=True, mode="json", exclude_none=True ) + logger.debug(f"NEIL --------- DC sending message: {msg_dict}") await room.local_participant.publish_data( json.dumps(msg_dict), topic=topic ) diff --git a/engine/src/nodes/core/tool/mcp.py b/engine/src/nodes/core/tool/mcp.py index dc37de93..f2692452 100644 --- a/engine/src/nodes/core/tool/mcp.py +++ b/engine/src/nodes/core/tool/mcp.py @@ -4,9 +4,11 @@ import asyncio import logging from typing import Any, cast +import contextlib -from core import node, pad +from core import node, pad, mcp from core.node import NodeMetadata +from mcp import ClientSession class MCP(node.Node): @@ -44,7 +46,56 @@ def resolve_pads(self): self.pads = [self_pad, mcp_server] async def run(self): - pass + while True: + exit_stack = contextlib.AsyncExitStack() + session = await self.create_session(exit_stack) + try: + await asyncio.wait_for(session.initialize(), timeout=2) + await asyncio.sleep(1) + except asyncio.TimeoutError: + logging.error("NEIL MCP Client session timeout") + await exit_stack.aclose() + continue + try: + tools = await asyncio.wait_for(session.list_tools(), timeout=10) + logging.info(f"NEIL Available tools: {tools}") + except Exception as e: + logging.error(f"NEIL MCP Client list_tools error: {e}") + await exit_stack.aclose() + continue - async def generate_mcp_server(self): - pass + async def create_session(self, exit_stack: contextlib.AsyncExitStack): + mcp_server_pad = cast(pad.PropertySinkPad, self.get_pad("mcp_server")) + if not mcp_server_pad or not mcp_server_pad.get_value(): + raise ValueError("MCP server pad not configured") + + mcp_server_name = mcp_server_pad.get_value() + mcp_server = next( + (s for s in self.mcp_servers if s.name == mcp_server_name), None + ) + if not mcp_server: + raise ValueError(f"MCP server '{mcp_server_name}' not found") + + if not isinstance(mcp_server.transport, mcp.MCPTransportDatachannelProxy): + raise ValueError( + "Only MCPTransportDatachannelProxy is supported in this node" + ) + + logging.info(f"NEIL Connecting to MCP server '{mcp_server.name}'") + read_stream, write_stream = await exit_stack.enter_async_context( + mcp.datachannel_host(self.room, "mcp_proxy", mcp_server.name) + ) + session = await exit_stack.enter_async_context( + ClientSession(read_stream=read_stream, write_stream=write_stream) + ) + return session + + async def session_ping_loop(self, session: ClientSession): + try: + while True: + await asyncio.sleep(10) + logging.info("NEIL MCP Client sending ping") + await asyncio.wait_for(session.send_ping(), timeout=2) + except asyncio.CancelledError: + logging.info("NEIL MCP Client ping loop cancelled") + raise diff --git a/engine/src/nodes/llm/base_llm.py b/engine/src/nodes/llm/base_llm.py index ef80c128..31795b85 100644 --- a/engine/src/nodes/llm/base_llm.py +++ b/engine/src/nodes/llm/base_llm.py @@ -34,7 +34,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.Trigger()], ) - self.pads.append(run_trigger) started_source = cast(pad.StatelessSourcePad, self.get_pad("started")) if not started_source: @@ -44,7 +43,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.Trigger()], ) - self.pads.append(started_source) tool_calls_started_source = cast( pad.StatelessSourcePad, self.get_pad("tool_calls_started") @@ -56,7 +54,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.Trigger()], ) - self.pads.append(tool_calls_started_source) tool_calls_finished_source = cast( pad.StatelessSourcePad, self.get_pad("tool_calls_finished") @@ -68,7 +65,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.Trigger()], ) - self.pads.append(tool_calls_finished_source) first_token_source = cast(pad.StatelessSourcePad, self.get_pad("first_token")) if not first_token_source: @@ -78,7 +74,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.Trigger()], ) - self.pads.append(first_token_source) text_stream_source = cast(pad.StatelessSourcePad, self.get_pad("text_stream")) if not text_stream_source: @@ -88,7 +83,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.TextStream()], ) - self.pads.append(text_stream_source) thinking_stream_source = cast( pad.StatelessSourcePad, self.get_pad("thinking_stream") @@ -100,7 +94,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.TextStream()], ) - self.pads.append(thinking_stream_source) context_message_source = cast( pad.StatelessSourcePad, self.get_pad("context_message") @@ -112,7 +105,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.ContextMessage()], ) - self.pads.append(context_message_source) finished_source = cast(pad.StatelessSourcePad, self.get_pad("finished")) if not finished_source: @@ -122,7 +114,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.Trigger()], ) - self.pads.append(finished_source) cancel_trigger = cast(pad.StatelessSinkPad, self.get_pad("cancel_trigger")) if not cancel_trigger: @@ -132,7 +123,6 @@ def resolve_pads(self): owner_node=self, default_type_constraints=[pad.types.Trigger()], ) - self.pads.append(cancel_trigger) context_sink = cast(pad.PropertySinkPad, self.get_pad("context")) if not context_sink: @@ -155,10 +145,10 @@ def resolve_pads(self): ) ], ) - self.pads.append(context_sink) + tool_group_sink = cast(pad.PropertySinkPad, self.get_pad("tool_group")) + mcp_pads: list[pad.PropertySinkPad] = [] if self.supports_tool_calls(): - tool_group_sink = cast(pad.PropertySinkPad, self.get_pad("tool_group")) if not tool_group_sink: tool_group_sink = pad.PropertySinkPad( id="tool_group", @@ -169,7 +159,61 @@ def resolve_pads(self): ], value=None, ) - self.pads.append(tool_group_sink) + + mcp_pads = self.mcp_server_pads() + + base_sink_pads = [ + run_trigger, + cancel_trigger, + context_sink, + ] + base_source_pads = [ + started_source, + first_token_source, + text_stream_source, + thinking_stream_source, + context_message_source, + finished_source, + ] + + if tool_group_sink is not None: + base_sink_pads.append(tool_group_sink) + base_sink_pads += mcp_pads + base_source_pads.append(tool_calls_started_source) + base_source_pads.append(tool_calls_finished_source) + + self.pads = base_sink_pads + base_source_pads + + def mcp_server_pads(self): + exising_mcp_pads = [ + p + for p in self.pads + if isinstance(p, pad.PropertySinkPad) + and p.get_id().startswith("mcp_server_") + ] + + connected_mcp_pads = [ + p for p in exising_mcp_pads if p.get_previous_pad() is not None + ] + + renamed_connected_mcp_pads = [] + for i, cp in enumerate(connected_mcp_pads): + new_id = f"mcp_server_{i}" + if cp.get_id() != new_id: + cp.set_id(new_id) + renamed_connected_mcp_pads.append(cp) + + empty_pad = pad.PropertySinkPad( + id=f"mcp_server_{len(renamed_connected_mcp_pads)}", + group="mcp_server", + owner_node=self, + default_type_constraints=[ + pad.types.NodeReference(node_types=["MCP"]), + ], + value="None", + ) + + return renamed_connected_mcp_pads + [empty_pad] async def run(self): cancel_trigger = cast( diff --git a/engine/src/services/repository/repository.py b/engine/src/services/repository/repository.py index defeabbb..93123e21 100644 --- a/engine/src/services/repository/repository.py +++ b/engine/src/services/repository/repository.py @@ -758,7 +758,7 @@ async def mcp_proxy_connection(self, request: aiohttp.web.Request): can_publish_data=True, can_subscribe=True, ) - ).with_identity(f"mcp_proxy-{short_uuid()}") + ).with_identity("mcp_proxy") connection_details = models.AppRunConnectionDetails( url=livekit_url, diff --git a/mcp_proxy_client/src/mcp_proxy/__pycache__/datachannel_transport.cpython-312.pyc b/mcp_proxy_client/src/mcp_proxy/__pycache__/datachannel_transport.cpython-312.pyc index 2a1d5ee14a7878742d11817258cdf51fe8f0232c..b220d54518a36ad1e5dfc8ae59874f4a85435bf9 100644 GIT binary patch delta 906 zcmZ{gUr19?9LLYO=bn3aH?QpfA4FX%!9UY-iJHQ|t;k~d;FI>$@{YBkPVTNnHOmkR z%l^nAMfyiiB}RW9>aEDlAn3)QhcOC*UJ`n^rsy7e=p1F0K|eU(^ZETh_pUg9IOX@U zYy{M1UM`-Te=eWmjEH(Rde)wiYh0j#XW5;W6nFZ}%YdYTG@(k5sg%pW1-Q*)+$2iM zL_xbuu%}KeU@bM07$C2ZUnuZOREv@o^cEf@PkAT#%tzo7smEPhk|S?8=f2zEE>{`^ zfrfvN2bxgg1#X*Ek7gi?xXPXZJKLs?W&l~mwcQF*cm+Mi3+yYYjY0Y}^EUsjN%jZ+ zG0jm5B%P#2sIC!8dnbSb&e4f8Xck?<7(g)fpbbntq~{(<2>q>M&)GoFp(ByT|5$G< zs(90}bUKk5_72fI8jp2&$&wJUloiCmR6L#-ju)^JJ24t37SUxW2rL*=NuwBq6=YEC zf-dqz>^xjW-r7wswP94#*DcA6>b4p85@iD-Ch=pR{QC_4QBl?)N05!V#WbTYqKqoX0xr3McSdCT?b4w zk>g=lhR9?0t0pgFC28%ryc* Py4-iMZ{0+_ttr0%H2TdE delta 970 zcmY*XTTc@~7@gT`cT0hmYb&h_;Q{0hF)AgE6irMl#P9^7p`~4+1=?o2ASj@Q1XD$; z8AXX26B8o@67?UD8ei}Qj1OdkiI3{jatUCf_UrZ(u z;IXl})OY!#X_yijta9+&+C~qkdbA01yWkiIab(re@dCV}7T`e+0@aYNhEk4cbr3_Q zqnaqqL1WcO zH0am3fkOYuBu62VlCBZDcLF4gC>3HapqQlm8YB_iDnRKU%GhM&+22gStd-33+jiX^%t( zH9Dp$N(t6VErxlZ93$L{zKa)HC78V?ni6I4GM&c!N2Xb4anw0bSoguPRK8kwakaps e^CV{g)|$DA)k2pp5GevqxZ823W7~*T%JVOw2j`~% diff --git a/mcp_proxy_client/src/mcp_proxy/__pycache__/mcp_proxy.cpython-312.pyc b/mcp_proxy_client/src/mcp_proxy/__pycache__/mcp_proxy.cpython-312.pyc index cd5dc642766ccf60f875e0d9e4074c04146bd925..bfb18f56b1cfb9fc2ed1403836d15a69d8f59e90 100644 GIT binary patch delta 561 zcmdm@x=)txG%qg~0}!ZhKbUcXb0gm$W+n;d$viCn%#20Ro8wqMF>;ouf)oJ3WJPxI z$%|OTC(mUU=BQz+VP3p zDG~;1zr~T8To9j^n45Y_Fux=twJ5$QH8CZ=xCBV$-V%Zcl^11}q(a52gnXR?0*dl0 zDizW)^D>JwQd1N(MJIQ0EwCx#2O0=5l?7y?CQFeNNJIl<4>z*8Jjf;%vjF8MPv=%- za%Y^pi~C9a6Cte|qLR}iCq~|ol$|dN1LcVgM3gB>;PUpZNd) delta 1224 zcmZ`%O>7fK7~NT~?PNW6&Bk^t$MH{uo49ryqTo0#X^NY)$ZI=6MO-)#xv@hxvAxl* z5v}FQQctNAsZ>*`La3sg3Ze)Ql^QsJiy#zwSu3WSZaHw^-UuqSNqgw*I!;=t9ckaE z_kHiZnOV)9-fshzAI)Z(z>Dtu{{1z0!}6DoEb9@ZW^|-byLpm&0T8JV1$tDCx)%1g z>OZkcR(0R#uaF2DJg`d4cD0A-B;`dv8{*C?vG8iUxmOoL=vUf>uF?#;ZSVpkS~9$b zKBxBC|4Tc#qqUMBQED#eQ03wCM6R<+ohQDeu7FlDApU2!p}hpXmO!fsHZ4K_?*wgZ z(#}H}ZTruXE5;ac7OY&<@PvkRj99sYKmMxj3hfva$(Q@-Ndl26QwBFC3}^Sto7uj0>49z81XAAGpm=h}vRPSyA-_Fc)6 zz!#UwVyjJJcNZR6TlO-~7g`fkH`;*{{bc;ArkE!ST)tF3*4k6(#(ztpO4(vwq4)Mt zV1X0MvpeYutuZYxXb@c3BJ``^SD{Z$Ft(+7O(hy|)2A&#eKU$qI9;m1%;dh-)gqXx zX0ew?#{ zyFN{Ql9F6wtG2s6w|eAwvJp>8W31#%HDP*#aeNW}EG#p@1`}LoLNW|VF!U5MEz_aZ zsk^CLDS15I7*9)wGLmz;S%WhhZr_FUxwPz#Hr&y5cT9G~mXGY3hOSfJnQoZm1BV+2 z4ogFcI#WMVk4w%>6HafqJQu>}!m?|);Tm3djmWl<<+pd8gO_sG7B4T#k%>lRLK=K? zRj3E*-I6oegzWS7nb2lucP#@5m;#%S9AvgOI}9o^_|99KPQo1Cdgh4z@ys@|EeiOz zef*Y}9Rv6LfE}Rj^(6){xlaQ&MBO(fIx%@*18jtPU{5fZtn~wSm|7c1m@#=61?)lU z;b_8&$s+?`N2y1~L?0%-k3#^L#}N&OH9V-{D8`#7=mp@2;=L=cv>#t2J6GVvJc|_5 zpN0`U5~AoA=m7z=4L#0&eFD6E7Z~++p;w(T7=j)lhK9@m5Jbn#x5wjZKY&3oy_Xfa wxk&z(}JWEn7x~=jLViL>p_FCbJO=D92n?F!Z^5Nv5C8xG diff --git a/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py b/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py index 52107019..a30de93c 100644 --- a/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py +++ b/mcp_proxy_client/src/mcp_proxy/datachannel_transport.py @@ -23,7 +23,6 @@ async def datachannel_client_proxy( def on_message(packet: rtc.DataPacket): if packet.topic != topic: return - logger.debug(f"Received data packet: {packet.data}") json_msg = types.JSONRPCMessage.model_validate_json(packet.data) sm = SessionMessage(json_msg) other_write_stream.send_nowait(sm) @@ -31,6 +30,9 @@ def on_message(packet: rtc.DataPacket): async def read_loop(): async with other_read_stream: async for session_message in other_read_stream: + logging.debug( + f"NEIL------------------------Read session message: {session_message}" + ) if isinstance(session_message, Exception): logger.error(f"Error in received message: {session_message}") continue diff --git a/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py b/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py index d10540a2..4a579b31 100644 --- a/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py +++ b/mcp_proxy_client/src/mcp_proxy/mcp_proxy.py @@ -50,25 +50,14 @@ async def run(self): ) else: raise ValueError(f"Unsupported local transport: {local_transport}") - self.session = await self.exit_stack.enter_async_context( - mcp.ClientSession(read_stream=read_stream, write_stream=write_stream) + + await datachannel_client_proxy( + room=self.room, + mcp_name=self.server.name, + other_read_stream=read_stream, + other_write_stream=write_stream, ) - # self.proxy_task = asyncio.create_task( - # datachannel_client_proxy( - # room=self.room, - # mcp_name=self.server.name, - # other_read_stream=read_stream, - # other_write_stream=write_stream, - # ) - # ) - logger.info(f"Initializing mcp session for: {self.server.name}") - await self.session.initialize() - logger.info(f"MCPProxy session initialized for {self.server.name}") - prompts = await self.session.list_prompts() - logger.info(f"MCPProxy prompts for {self.server.name}: {prompts}") - tools = await self.session.list_tools() - logger.info(f"MCPProxy tools for {self.server.name}: {tools}") - # await self.proxy_task + logger.info(f"MCPProxy finished {self.server.name}") async def aclose(self): From 6b6209334f83d353e084c5033b79c686798ba2c9 Mon Sep 17 00:00:00 2001 From: Keepingitneil Date: Wed, 10 Sep 2025 22:29:24 -0700 Subject: [PATCH 6/9] mcp tool calling work --- engine/src/core/runtime_types/types.py | 45 ++++- engine/src/nodes/core/tool/mcp.py | 67 +++++-- engine/src/nodes/core/tool/tool_group.py | 6 + engine/src/nodes/llm/base_llm.py | 167 ++++++++++++------ engine/src/nodes/llm/openai_compatible_llm.py | 15 +- 5 files changed, 233 insertions(+), 67 deletions(-) diff --git a/engine/src/core/runtime_types/types.py b/engine/src/core/runtime_types/types.py index c9638af1..626e9e05 100644 --- a/engine/src/core/runtime_types/types.py +++ b/engine/src/core/runtime_types/types.py @@ -275,7 +275,13 @@ class ContextMessageContent_ToolCallDelta(BaseModel): class Schema(BaseModel): properties: dict[ - str, pad.types.String | pad.types.Integer | pad.types.Float | pad.types.Boolean + str, + pad.types.String + | pad.types.Integer + | pad.types.Float + | pad.types.Boolean + | pad.types.Object + | pad.types.List, ] required: list[str] | None = None defaults: dict[str, Any] | None = None @@ -295,6 +301,43 @@ def to_json_schema(self) -> dict[str, Any]: "required": self.required or [], } + @classmethod + def from_json_schema(cls, schema: dict[str, Any]) -> "Schema": + properties: dict[ + str, + pad.types.String + | pad.types.Integer + | pad.types.Float + | pad.types.Boolean + | pad.types.Object + | pad.types.List, + ] = {} + for key, value in schema.get("properties", {}).items(): + type_ = value.get("type") + if type_ == "string": + properties[key] = pad.types.String() + elif type_ == "integer": + properties[key] = pad.types.Integer() + elif type_ == "number": + properties[key] = pad.types.Float() + elif type_ == "boolean": + properties[key] = pad.types.Boolean() + elif type_ == "object": + properties[key] = pad.types.Object() + elif type_ == "array": + properties[key] = pad.types.List(item_type_constraints=None) + else: + raise ValueError(f"Unsupported property type: {type_}") + return cls( + properties=properties, + required=schema.get("required", []), + defaults={ + k: v.get("default") + for k, v in schema.get("properties", {}).items() + if "default" in v + }, + ) + def intersect(self, other: "Schema"): if not isinstance(other, Schema): return None diff --git a/engine/src/nodes/core/tool/mcp.py b/engine/src/nodes/core/tool/mcp.py index f2692452..2d7aae04 100644 --- a/engine/src/nodes/core/tool/mcp.py +++ b/engine/src/nodes/core/tool/mcp.py @@ -3,11 +3,12 @@ import asyncio import logging -from typing import Any, cast +from typing import Tuple, cast import contextlib -from core import node, pad, mcp +from core import node, pad, mcp, runtime_types from core.node import NodeMetadata +from mcp.types import ContentBlock from mcp import ClientSession @@ -46,19 +47,22 @@ def resolve_pads(self): self.pads = [self_pad, mcp_server] async def run(self): + self.init_lock = asyncio.Lock() + self.session: ClientSession | None = None while True: exit_stack = contextlib.AsyncExitStack() - session = await self.create_session(exit_stack) - try: - await asyncio.wait_for(session.initialize(), timeout=2) - await asyncio.sleep(1) - except asyncio.TimeoutError: - logging.error("NEIL MCP Client session timeout") - await exit_stack.aclose() - continue + async with self.init_lock: + self.session = await self.create_session(exit_stack) + try: + await asyncio.wait_for(self.session.initialize(), timeout=2) + await asyncio.sleep(1) + except asyncio.TimeoutError: + logging.error("NEIL MCP Client session timeout") + await exit_stack.aclose() + continue + try: - tools = await asyncio.wait_for(session.list_tools(), timeout=10) - logging.info(f"NEIL Available tools: {tools}") + await self.session_ping_loop(self.session) except Exception as e: logging.error(f"NEIL MCP Client list_tools error: {e}") await exit_stack.aclose() @@ -99,3 +103,42 @@ async def session_ping_loop(self, session: ClientSession): except asyncio.CancelledError: logging.info("NEIL MCP Client ping loop cancelled") raise + + async def to_tool_definitions(self) -> list[runtime_types.ToolDefinition]: + async with self.init_lock: + if not self.session: + raise ValueError("MCP session not initialized") + mcp_tools_res = await self.session.list_tools() + mcp_tools = mcp_tools_res.tools + tool_defs: list[runtime_types.ToolDefinition] = [] + for t in mcp_tools: + schema = runtime_types.Schema.from_json_schema(t.inputSchema) + tool_def = runtime_types.ToolDefinition( + name=t.name, + description=t.description or "", + parameters=schema, + ) + tool_defs.append(tool_def) + + return tool_defs + + async def call_tools(self, tool_calls: list[runtime_types.ToolCall]): + async with self.init_lock: + if not self.session: + raise ValueError("MCP session not initialized") + results: list[ + Tuple[runtime_types.ToolCall, list[ContentBlock] | Exception] + ] = [] + for tc in tool_calls: + try: + response = await asyncio.wait_for( + self.session.call_tool(tc.name, tc.arguments), timeout=15 + ) + results.append((tc, response.content)) + except asyncio.TimeoutError: + results.append((tc, Exception(f"Tool call '{tc.name}' timed out."))) + except Exception as e: + results.append( + (tc, Exception(f"Error calling tool '{tc.name}': {e}")) + ) + return results diff --git a/engine/src/nodes/core/tool/tool_group.py b/engine/src/nodes/core/tool/tool_group.py index 3de2af76..37a2e57c 100644 --- a/engine/src/nodes/core/tool/tool_group.py +++ b/engine/src/nodes/core/tool/tool_group.py @@ -83,6 +83,12 @@ def resolve_pads(self): self.pads = [num_tools, self_pad] + tools + def has_tool(self, tool_name: str) -> bool: + for tool in self.tool_nodes: + if tool.get_name() == tool_name: + return True + return False + async def call_tools( self, tool_calls: list[runtime_types.ToolCall], diff --git a/engine/src/nodes/llm/base_llm.py b/engine/src/nodes/llm/base_llm.py index 31795b85..91b61cbe 100644 --- a/engine/src/nodes/llm/base_llm.py +++ b/engine/src/nodes/llm/base_llm.py @@ -4,12 +4,21 @@ import asyncio import logging from abc import ABC, abstractmethod -from typing import cast +from typing import cast, Tuple from core import node, pad, runtime_types +from nodes.core.tool import mcp from lib.llm import AsyncLLMResponseHandle, LLMRequest, openai_compatible from utils import get_full_content_from_deltas, get_tool_calls_from_choice_deltas from nodes.core.tool import Tool, ToolGroup +from mcp.types import ( + ContentBlock, + TextContent, + ImageContent, + AudioContent, + ResourceLink, + EmbeddedResource, +) class BaseLLM(node.Node, ABC): @@ -25,7 +34,7 @@ def model(self) -> str: ... @abstractmethod async def api_key(self) -> str: ... - def resolve_pads(self): + def get_base_pads(self): run_trigger = cast(pad.StatelessSinkPad, self.get_pad("run_trigger")) if not run_trigger: run_trigger = pad.StatelessSinkPad( @@ -162,12 +171,12 @@ def resolve_pads(self): mcp_pads = self.mcp_server_pads() - base_sink_pads = [ + base_sink_pads: list[pad.SinkPad] = [ run_trigger, cancel_trigger, context_sink, ] - base_source_pads = [ + base_source_pads: list[pad.SourcePad] = [ started_source, first_token_source, text_stream_source, @@ -182,7 +191,7 @@ def resolve_pads(self): base_source_pads.append(tool_calls_started_source) base_source_pads.append(tool_calls_finished_source) - self.pads = base_sink_pads + base_source_pads + return base_sink_pads, base_source_pads def mcp_server_pads(self): exising_mcp_pads = [ @@ -229,12 +238,6 @@ async def run(self): finished_source = cast( pad.StatelessSourcePad, self.get_pad_required("finished") ) - tool_calls_started_source = cast( - pad.StatelessSourcePad, self.get_pad_required("tool_calls_started") - ) - tool_calls_finished_source = cast( - pad.StatelessSourcePad, self.get_pad_required("tool_calls_finished") - ) context_sink = cast(pad.PropertySinkPad, self.get_pad_required("context")) context_message_source = cast( pad.StatelessSourcePad, self.get_pad_required("context_message") @@ -289,9 +292,12 @@ async def cancel_task(): item.ctx.complete() async def generation_task( - handle: AsyncLLMResponseHandle, ctx: pad.RequestContext + handle: AsyncLLMResponseHandle, + ctx: pad.RequestContext, + tg_tools: list[runtime_types.ToolDefinition], + mcp_tools: dict[mcp.MCP, list[runtime_types.ToolDefinition]], ): - tool_task: asyncio.Task[list[str]] | None = None + tool_task: asyncio.Task[list[runtime_types.ContextMessage]] | None = None all_deltas: list[runtime_types.ContextMessageContent_ChoiceDelta] = [] text_stream = runtime_types.TextStream() thinking_stream = runtime_types.TextStream() @@ -332,14 +338,14 @@ async def generation_task( all_tool_calls: list[runtime_types.ToolCall] = [] if tool_group_sink is not None: all_tool_calls = get_tool_calls_from_choice_deltas(all_deltas) - if len(all_tool_calls) > 0: - tool_calls_started_source.push_item( - runtime_types.Trigger(), ctx - ) - tg = cast(ToolGroup, tool_group_sink.get_value()) - tool_task = asyncio.create_task( - tg.call_tools(all_tool_calls, ctx) + tool_task = asyncio.create_task( + self.call_tools( + all_tool_calls=all_tool_calls, + tg_tool_defns=tg_tool_definitions, + mcp_tool_defns=mcp_tools, + ctx=ctx, ) + ) full_content = get_full_content_from_deltas(all_deltas) context_message_source.push_item( @@ -356,33 +362,29 @@ async def generation_task( ) if tool_task is not None: - tool_results = await tool_task - for i in range(len(all_tool_calls)): - context_message_source.push_item( - runtime_types.ContextMessage( - role=runtime_types.ContextMessageRole.TOOL, - content=[ - runtime_types.ContextMessageContentItem_Text( - content=tool_results[i] - ) - ], - tool_call_id=all_tool_calls[i].call_id, - tool_calls=[], - ), - ctx, - ) - tool_calls_finished_source.push_item(runtime_types.Trigger(), ctx) + tool_msgs = await tool_task + for msg in tool_msgs: + context_message_source.push_item(msg, ctx) + # TODO + # for i in range(len(all_tool_calls)): + # context_message_source.push_item( + # runtime_types.ContextMessage( + # role=runtime_types.ContextMessageRole.TOOL, + # content=[ + # runtime_types.ContextMessageContentItem_Text( + # content=tool_results[i] + # ) + # ], + # tool_call_id=all_tool_calls[i].call_id, + # tool_calls=[], + # ), + # ctx, + # ) # TODO look at the finished/ctx.complete() logic here except asyncio.CancelledError: - finished_source.push_item(runtime_types.Trigger(), ctx) - ctx.complete() + pass except Exception as e: logging.error(f"Error during LLM generation: {e}", exc_info=e) - if tool_task is not None: - tool_calls_finished_source.push_item(runtime_types.Trigger(), ctx) - - finished_source.push_item(runtime_types.Trigger(), ctx) - ctx.complete() finally: finished_source.push_item(runtime_types.Trigger(), ctx) ctx.complete() @@ -399,16 +401,28 @@ def done_callback(task: asyncio.Task): async for item in run_trigger: ctx = item.ctx messages = context_sink.get_value() - tool_definitions: list[runtime_types.ToolDefinition] = [] + all_tool_definitions: list[runtime_types.ToolDefinition] = [] + tg_tool_definitions: list[runtime_types.ToolDefinition] = [] if tool_group_sink is not None and tool_group_sink.get_value() is not None: tool_nodes = cast(ToolGroup, tool_group_sink.get_value()).tool_nodes for tn in tool_nodes: - if not isinstance(tn, Tool): - logging.warning(f"Node {tn.id} is not a Tool, skipping.") - continue td = tn.get_tool_definition() - tool_definitions.append(td) - request = LLMRequest(context=messages, tool_definitions=tool_definitions) + tg_tool_definitions.append(td) + all_tool_definitions.append(td) + mcp_tool_definitions: dict[mcp.MCP, list[runtime_types.ToolDefinition]] = {} + mcp_sinks = self.mcp_server_pads() + for mcp_sink in mcp_sinks: + mcp_node = cast(mcp.MCP, mcp_sink.get_value()) + if mcp_node not in mcp_tool_definitions: + mcp_tool_definitions[mcp_node] = [] + tdfs = await mcp_node.to_tool_definitions() + for tdf in tdfs: + mcp_tool_definitions[mcp_node].append(tdf) + all_tool_definitions.append(tdf) + + request = LLMRequest( + context=messages, tool_definitions=all_tool_definitions + ) if running_handle is not None: logging.warning( "LLM is already running a generation, skipping new request." @@ -422,7 +436,14 @@ def done_callback(task: asyncio.Task): video_support=video_supported, audio_support=audio_supported, ) - t = asyncio.create_task(generation_task(running_handle, ctx)) + t = asyncio.create_task( + generation_task( + running_handle, + ctx, + tg_tools=tg_tool_definitions, + mcp_tools=mcp_tool_definitions, + ) + ) tasks.add(t) t.add_done_callback(done_callback) except Exception as e: @@ -430,6 +451,52 @@ def done_callback(task: asyncio.Task): finished_source.push_item(runtime_types.Trigger(), ctx) await cancel_task_t + async def call_tools( + self, + *, + tg_tool_defns: list[runtime_types.ToolDefinition], + mcp_tool_defns: dict[mcp.MCP, list[runtime_types.ToolDefinition]], + all_tool_calls: list[runtime_types.ToolCall], + ctx: pad.RequestContext, + ) -> list[runtime_types.ContextMessage]: + tg_tool_calls = [t for t in all_tool_calls if t.name in tg_tool_defns] + + if len(tg_tool_calls) > 0: + tg_task = asyncio.create_task( + self.call_tg_calls(tg_tool_calls=tg_tool_calls, ctx=ctx) + ) + + mcp_tool_calls: dict[mcp.MCP, list[runtime_types.ToolCall]] = {} + mcp_tasks: list[ + asyncio.Task[Tuple[runtime_types.ToolCall, list[ContentBlock] | Exception]] + ] = [] + for mcp_node in mcp_tool_defns.keys(): + mcp_defns = mcp_tool_defns[mcp_node] + mcp_calls = [tc for tc in all_tool_calls if tc.name in mcp_defns] + mcp_tasks.append( + asyncio.create_task( + self.call_mcp_tools( + mcp_node=mcp_node, + mcp_tool_calls=mcp_calls, + ctx=ctx, + ) + ) + ) + + return [] + + async def call_tg_calls(self, tg_tool_calls: list[runtime_types.ToolCall], ctx): + tool_group_sink = cast(pad.PropertySinkPad, self.get_pad_required("tool_group")) + if tool_group_sink is None or tool_group_sink.get_value() is None: + raise ValueError("Tool group is not configured for tool calls.") + tg = cast(ToolGroup, tool_group_sink.get_value()) + return await tg.call_tools(tg_tool_calls, ctx) + + async def call_mcp_tools( + self, mcp_node: mcp.MCP, mcp_tool_calls: list[runtime_types.ToolCall], ctx + ): + return await mcp_node.call_tools(mcp_tool_calls) + async def _supports_video(self, llm: openai_compatible.OpenAICompatibleLLM) -> bool: dummy_request = LLMRequest( context=[ diff --git a/engine/src/nodes/llm/openai_compatible_llm.py b/engine/src/nodes/llm/openai_compatible_llm.py index b2298dfa..cd1d5a61 100644 --- a/engine/src/nodes/llm/openai_compatible_llm.py +++ b/engine/src/nodes/llm/openai_compatible_llm.py @@ -1,6 +1,7 @@ # Copyright 2025 Fluently AI, Inc. DBA Gabber. All rights reserved. # SPDX-License-Identifier: SUL-1.0 +import logging from typing import cast from core import pad @@ -47,7 +48,8 @@ async def api_key(self) -> str: return await self.secret_provider.resolve_secret(api_key_name) def resolve_pads(self): - super().resolve_pads() + logging.info("NEIL ________ Resolving pads for OpenAICompatibleLLM") + base_sink_pads, base_source_pads = self.get_base_pads() base_url_sink = cast(pad.PropertySinkPad, self.get_pad("base_url")) if not base_url_sink: base_url_sink = pad.PropertySinkPad( @@ -57,7 +59,6 @@ def resolve_pads(self): default_type_constraints=[pad.types.String()], value="https://api.openai.com/v1", ) - self.pads.append(base_url_sink) api_key_sink = cast(pad.PropertySinkPad, self.get_pad("api_key")) if not api_key_sink: @@ -68,7 +69,6 @@ def resolve_pads(self): default_type_constraints=[pad.types.Secret(options=[])], value="", ) - self.pads.append(api_key_sink) model_sink = cast(pad.PropertySinkPad, self.get_pad("model")) if not model_sink: @@ -79,8 +79,15 @@ def resolve_pads(self): default_type_constraints=[pad.types.String()], value="gpt-4.1-mini", ) - self.pads.append(model_sink) + + base_sink_pads.extend([base_url_sink, api_key_sink, model_sink]) cast(list[pad.types.Secret], api_key_sink.get_type_constraints())[ 0 ].options = self.secrets + + logging.info( + f"NEIL ________ Resolved pads for {[p.get_id() for p in base_sink_pads]}:{[p.get_id() for p in base_source_pads]}" + ) + + self.pads = cast(list[pad.Pad], base_sink_pads + base_source_pads) From e0d218098d23d01e510f4aacbb950a8067a60f89 Mon Sep 17 00:00:00 2001 From: Keepingitneil Date: Thu, 11 Sep 2025 12:26:00 -0700 Subject: [PATCH 7/9] hello world working --- engine/pyproject.toml | 2 +- engine/src/core/mcp/datachannel_transport.py | 2 - engine/src/core/pad/types.py | 58 ++++++++++ engine/src/core/runtime_types/types.py | 14 ++- engine/src/lib/llm/llm.py | 11 +- engine/src/nodes/core/tool/mcp.py | 37 ++++--- engine/src/nodes/llm/base_llm.py | 102 ++++++++++-------- engine/src/nodes/llm/openai_compatible_llm.py | 5 - engine/uv.lock | 8 +- 9 files changed, 160 insertions(+), 79 deletions(-) diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 35330bfe..e276bade 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "elevenlabs==1.12.1", "pyvips~=2.0.2", "av~=14.4.0", - "openai~=1.93.0", + "openai~=1.107.1", "pillow~=11.1.0", "msgpack~=1.1.1", "python-dotenv~=1.1.1", diff --git a/engine/src/core/mcp/datachannel_transport.py b/engine/src/core/mcp/datachannel_transport.py index c1c42bb6..aa5cd56c 100644 --- a/engine/src/core/mcp/datachannel_transport.py +++ b/engine/src/core/mcp/datachannel_transport.py @@ -58,7 +58,6 @@ async def on_message_loop(): try: message = types.JSONRPCMessage.model_validate_json(packet.data) - logging.info(f"NEIL message received on topic {message}") session_message = SessionMessage(message) await read_stream_writer.send(session_message) except ValidationError as exc: @@ -80,7 +79,6 @@ async def dc_writer(): msg_dict = session_message.message.model_dump( by_alias=True, mode="json", exclude_none=True ) - logger.debug(f"NEIL --------- DC sending message: {msg_dict}") await room.local_participant.publish_data( json.dumps(msg_dict), topic=topic ) diff --git a/engine/src/core/pad/types.py b/engine/src/core/pad/types.py index 38905a81..a7719b5b 100644 --- a/engine/src/core/pad/types.py +++ b/engine/src/core/pad/types.py @@ -19,6 +19,9 @@ def intersect(self, other: "BasePadType") -> "BasePadType | None": return self return None + def to_json_schema(self) -> dict[str, Any]: + raise NotImplementedError() + class String(BasePadType): type: Literal["string"] = "string" @@ -43,6 +46,14 @@ def intersect(self, other: "BasePadType"): ), ) + def to_json_schema(self) -> dict[str, Any]: + schema: dict[str, Any] = {"type": "string"} + if self.max_length is not None: + schema["maxLength"] = self.max_length + if self.min_length is not None: + schema["minLength"] = self.min_length + return schema + class Enum(BasePadType): type: Literal["enum"] = "enum" @@ -75,6 +86,12 @@ def intersect(self, other: "BasePadType"): raise ValueError("Unexpected state.") + def to_json_schema(self) -> dict[str, Any]: + schema: dict[str, Any] = {"type": "string"} + if self.options is not None: + schema["enum"] = self.options + return schema + class Secret(BasePadType): type: Literal["secret"] = "secret" @@ -96,6 +113,9 @@ def intersect(self, other: "BasePadType"): options=intersected_options, ) + def to_json_schema(self) -> dict[str, Any]: + raise NotImplementedError() + class Integer(BasePadType): type: Literal["integer"] = "integer" @@ -119,6 +139,14 @@ def intersect(self, other: "BasePadType"): ), ) + def to_json_schema(self) -> dict[str, Any]: + schema: dict[str, Any] = {"type": "integer"} + if self.maximum is not None: + schema["maximum"] = self.maximum + if self.minimum is not None: + schema["minimum"] = self.minimum + return schema + class Float(BasePadType): type: Literal["float"] = "float" @@ -142,6 +170,14 @@ def intersect(self, other: "BasePadType"): ), ) + def to_json_schema(self) -> dict[str, Any]: + schema: dict[str, Any] = {"type": "number"} + if self.maximum is not None: + schema["maximum"] = self.maximum + if self.minimum is not None: + schema["minimum"] = self.minimum + return schema + class BoundingBox(BasePadType): type: Literal["bounding_box"] = "bounding_box" @@ -154,6 +190,9 @@ class Point(BasePadType): class Boolean(BasePadType): type: Literal["boolean"] = "boolean" + def to_json_schema(self) -> dict[str, Any]: + return {"type": "boolean"} + class Audio(BasePadType): type: Literal["audio"] = "audio" @@ -211,6 +250,17 @@ def intersect(self, other: "BasePadType"): ), ) + def to_json_schema(self) -> dict[str, Any]: + schema: dict[str, Any] = {"type": "array"} + if self.max_length is not None: + schema["maxItems"] = self.max_length + if ( + self.item_type_constraints is not None + and len(self.item_type_constraints) == 1 + ): + schema["items"] = self.item_type_constraints[0].to_json_schema() + return schema + class Schema(BasePadType): type: Literal["schema"] = "schema" @@ -243,6 +293,14 @@ def intersect(self, other: "BasePadType"): return Object(object_schema=new_schema) if new_schema else None + def to_json_schema(self) -> dict[str, Any]: + if self.object_schema is not None: + return { + "type": "object", + "properties": self.object_schema, + } + return {"type": "object"} + class NodeReference(BasePadType): type: Literal["node_reference"] = "node_reference" diff --git a/engine/src/core/runtime_types/types.py b/engine/src/core/runtime_types/types.py index 626e9e05..bbc8c375 100644 --- a/engine/src/core/runtime_types/types.py +++ b/engine/src/core/runtime_types/types.py @@ -3,6 +3,7 @@ import asyncio import base64 +import logging from dataclasses import dataclass from enum import Enum from typing import Annotated, Any, Literal, cast @@ -198,7 +199,7 @@ class ToolCall(BaseModel): class ToolDefinition(BaseModel): name: str description: str - parameters: "Schema | None" = None + parameters: "Schema | dict[str, Any] | None" = None class ContextMessageContentItem_Audio(BaseModel): @@ -287,9 +288,7 @@ class Schema(BaseModel): defaults: dict[str, Any] | None = None def to_json_schema(self) -> dict[str, Any]: - properties = { - k: v.model_dump(exclude_none=True) for k, v in self.properties.items() - } + properties = {k: v.to_json_schema() for k, v in self.properties.items()} for d in self.defaults or {}: if self.defaults is None: continue @@ -343,7 +342,12 @@ def intersect(self, other: "Schema"): return None properties: dict[ str, - pad.types.String | pad.types.Integer | pad.types.Float | pad.types.Boolean, + pad.types.String + | pad.types.Integer + | pad.types.Float + | pad.types.Boolean + | pad.types.Object + | pad.types.List, ] = {} defaults: dict[str, Any] = {} for d in self.defaults or {}: diff --git a/engine/src/lib/llm/llm.py b/engine/src/lib/llm/llm.py index fb1560b3..8a72b615 100644 --- a/engine/src/lib/llm/llm.py +++ b/engine/src/lib/llm/llm.py @@ -23,6 +23,7 @@ ContextMessageContentItem_Video, ContextMessageRole, ToolDefinition, + Schema, ) from lib.video.mp4_encoder import MP4_Encoder @@ -182,8 +183,16 @@ def to_openai_completion_tools_input(self) -> list[chat.ChatCompletionToolParam] "properties": {}, "required": [], } - else: + elif isinstance(tool.parameters, dict): + parameters = tool.parameters + elif isinstance(tool.parameters, Schema): parameters = tool.parameters.to_json_schema() + else: + parameters = { + "type": "object", + "properties": {}, + "required": [], + } tools.append( { diff --git a/engine/src/nodes/core/tool/mcp.py b/engine/src/nodes/core/tool/mcp.py index 2d7aae04..aa18d7d1 100644 --- a/engine/src/nodes/core/tool/mcp.py +++ b/engine/src/nodes/core/tool/mcp.py @@ -112,33 +112,32 @@ async def to_tool_definitions(self) -> list[runtime_types.ToolDefinition]: mcp_tools = mcp_tools_res.tools tool_defs: list[runtime_types.ToolDefinition] = [] for t in mcp_tools: - schema = runtime_types.Schema.from_json_schema(t.inputSchema) tool_def = runtime_types.ToolDefinition( name=t.name, description=t.description or "", - parameters=schema, + parameters=t.inputSchema, ) tool_defs.append(tool_def) return tool_defs - async def call_tools(self, tool_calls: list[runtime_types.ToolCall]): + async def call_tool(self, tool_call: runtime_types.ToolCall): + logging.info(f"NEIL ************ MCP Client calling tool '{tool_call.name}'") + sess: ClientSession async with self.init_lock: if not self.session: raise ValueError("MCP session not initialized") - results: list[ - Tuple[runtime_types.ToolCall, list[ContentBlock] | Exception] - ] = [] - for tc in tool_calls: - try: - response = await asyncio.wait_for( - self.session.call_tool(tc.name, tc.arguments), timeout=15 - ) - results.append((tc, response.content)) - except asyncio.TimeoutError: - results.append((tc, Exception(f"Tool call '{tc.name}' timed out."))) - except Exception as e: - results.append( - (tc, Exception(f"Error calling tool '{tc.name}': {e}")) - ) - return results + sess = self.session + + results: list[ContentBlock] | Exception = [] + try: + response = await asyncio.wait_for( + sess.call_tool(tool_call.name, tool_call.arguments), timeout=15 + ) + results = response.content if response.content else [] + except asyncio.TimeoutError: + results = Exception(f"Tool call '{tool_call.name}' timed out.") + except Exception as e: + results = Exception(f"Error calling tool '{tool_call.name}': {e}") + + return results diff --git a/engine/src/nodes/llm/base_llm.py b/engine/src/nodes/llm/base_llm.py index 91b61cbe..34e9fe84 100644 --- a/engine/src/nodes/llm/base_llm.py +++ b/engine/src/nodes/llm/base_llm.py @@ -365,22 +365,6 @@ async def generation_task( tool_msgs = await tool_task for msg in tool_msgs: context_message_source.push_item(msg, ctx) - # TODO - # for i in range(len(all_tool_calls)): - # context_message_source.push_item( - # runtime_types.ContextMessage( - # role=runtime_types.ContextMessageRole.TOOL, - # content=[ - # runtime_types.ContextMessageContentItem_Text( - # content=tool_results[i] - # ) - # ], - # tool_call_id=all_tool_calls[i].call_id, - # tool_calls=[], - # ), - # ctx, - # ) - # TODO look at the finished/ctx.complete() logic here except asyncio.CancelledError: pass except Exception as e: @@ -413,12 +397,13 @@ def done_callback(task: asyncio.Task): mcp_sinks = self.mcp_server_pads() for mcp_sink in mcp_sinks: mcp_node = cast(mcp.MCP, mcp_sink.get_value()) + if not isinstance(mcp_node, mcp.MCP): + continue if mcp_node not in mcp_tool_definitions: mcp_tool_definitions[mcp_node] = [] tdfs = await mcp_node.to_tool_definitions() - for tdf in tdfs: - mcp_tool_definitions[mcp_node].append(tdf) - all_tool_definitions.append(tdf) + mcp_tool_definitions[mcp_node].extend(tdfs) + all_tool_definitions.extend(tdfs) request = LLMRequest( context=messages, tool_definitions=all_tool_definitions @@ -461,29 +446,67 @@ async def call_tools( ) -> list[runtime_types.ContextMessage]: tg_tool_calls = [t for t in all_tool_calls if t.name in tg_tool_defns] + results: list[runtime_types.ContextMessage] = [] + + all_tasks: list[asyncio.Task] = [] + + async def run_tg_task(): + tg_res = await self.call_tg_calls(tg_tool_calls=tg_tool_calls, ctx=ctx) + for i, res in enumerate(tg_res): + msg = runtime_types.ContextMessage( + role=runtime_types.ContextMessageRole.TOOL, + content=[runtime_types.ContextMessageContentItem_Text(content=res)], + tool_call_id=tg_tool_calls[i].call_id, + tool_calls=[], + ) + results.append(msg) + + async def run_mcp_task(node: mcp.MCP, tc: runtime_types.ToolCall): + res = await node.call_tool(tc) + if isinstance(res, Exception): + logging.error(f"Error calling MCP tool '{tc.name}': {res}") + msg = runtime_types.ContextMessage( + role=runtime_types.ContextMessageRole.TOOL, + content=[ + runtime_types.ContextMessageContentItem_Text( + content=f"Error calling tool '{tc.name}': {res}" + ) + ], + tool_call_id=tc.call_id, + tool_calls=[], + ) + else: + contents: list[runtime_types.ContextMessageContentItem] = [] + for block in res: + if isinstance(block, TextContent): + content = runtime_types.ContextMessageContentItem_Text( + content=block.text + ) + contents.append(content) + else: + logging.error(f"Error calling MCP tool '{tc.name}': {res}") + msg = runtime_types.ContextMessage( + role=runtime_types.ContextMessageRole.TOOL, + content=contents, + tool_call_id=tc.call_id, + tool_calls=[], + ) + results.append(msg) + if len(tg_tool_calls) > 0: - tg_task = asyncio.create_task( - self.call_tg_calls(tg_tool_calls=tg_tool_calls, ctx=ctx) - ) + tg_task = asyncio.create_task(run_tg_task()) + all_tasks.append(tg_task) - mcp_tool_calls: dict[mcp.MCP, list[runtime_types.ToolCall]] = {} - mcp_tasks: list[ - asyncio.Task[Tuple[runtime_types.ToolCall, list[ContentBlock] | Exception]] - ] = [] for mcp_node in mcp_tool_defns.keys(): mcp_defns = mcp_tool_defns[mcp_node] - mcp_calls = [tc for tc in all_tool_calls if tc.name in mcp_defns] - mcp_tasks.append( - asyncio.create_task( - self.call_mcp_tools( - mcp_node=mcp_node, - mcp_tool_calls=mcp_calls, - ctx=ctx, - ) - ) - ) + mcp_defn_names = [td.name for td in mcp_defns] + mcp_calls = [tc for tc in all_tool_calls if tc.name in mcp_defn_names] + for tc in mcp_calls: + mcp_t = asyncio.create_task(run_mcp_task(mcp_node, tc)) + all_tasks.append(mcp_t) - return [] + await asyncio.gather(*all_tasks) + return results async def call_tg_calls(self, tg_tool_calls: list[runtime_types.ToolCall], ctx): tool_group_sink = cast(pad.PropertySinkPad, self.get_pad_required("tool_group")) @@ -492,11 +515,6 @@ async def call_tg_calls(self, tg_tool_calls: list[runtime_types.ToolCall], ctx): tg = cast(ToolGroup, tool_group_sink.get_value()) return await tg.call_tools(tg_tool_calls, ctx) - async def call_mcp_tools( - self, mcp_node: mcp.MCP, mcp_tool_calls: list[runtime_types.ToolCall], ctx - ): - return await mcp_node.call_tools(mcp_tool_calls) - async def _supports_video(self, llm: openai_compatible.OpenAICompatibleLLM) -> bool: dummy_request = LLMRequest( context=[ diff --git a/engine/src/nodes/llm/openai_compatible_llm.py b/engine/src/nodes/llm/openai_compatible_llm.py index cd1d5a61..789eb6d4 100644 --- a/engine/src/nodes/llm/openai_compatible_llm.py +++ b/engine/src/nodes/llm/openai_compatible_llm.py @@ -48,7 +48,6 @@ async def api_key(self) -> str: return await self.secret_provider.resolve_secret(api_key_name) def resolve_pads(self): - logging.info("NEIL ________ Resolving pads for OpenAICompatibleLLM") base_sink_pads, base_source_pads = self.get_base_pads() base_url_sink = cast(pad.PropertySinkPad, self.get_pad("base_url")) if not base_url_sink: @@ -86,8 +85,4 @@ def resolve_pads(self): 0 ].options = self.secrets - logging.info( - f"NEIL ________ Resolved pads for {[p.get_id() for p in base_sink_pads]}:{[p.get_id() for p in base_source_pads]}" - ) - self.pads = cast(list[pad.Pad], base_sink_pads + base_source_pads) diff --git a/engine/uv.lock b/engine/uv.lock index 436c323a..3aac3377 100644 --- a/engine/uv.lock +++ b/engine/uv.lock @@ -376,7 +376,7 @@ requires-dist = [ { name = "nltk" }, { name = "numpy" }, { name = "onnxruntime", specifier = ">=1.17.1" }, - { name = "openai", specifier = "~=1.93.0" }, + { name = "openai", specifier = "~=1.107.1" }, { name = "opencv-python-headless", specifier = "~=4.12.0" }, { name = "pillow", specifier = "~=11.1.0" }, { name = "posthog" }, @@ -789,7 +789,7 @@ wheels = [ [[package]] name = "openai" -version = "1.93.3" +version = "1.107.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -801,9 +801,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/66/fadc0cad6a229c6a85c3aa5f222a786ec4d9bf14c2a004f80ffa21dbaf21/openai-1.93.3.tar.gz", hash = "sha256:488b76399238c694af7e4e30c58170ea55e6f65038ab27dbe95b5077a00f8af8", size = 487595, upload_time = "2025-07-09T14:08:27.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/e0/a62daa7ff769df969cc1b782852cace79615039630b297005356f5fb46fb/openai-1.107.1.tar.gz", hash = "sha256:7c51b6b8adadfcf5cada08a613423575258b180af5ad4bc2954b36ebc0d3ad48", size = 563671, upload_time = "2025-09-10T15:04:40.288Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/b9/0df6351b25c6bd494c534d2a8191dc9460fb5bb09c88b1427775d49fde05/openai-1.93.3-py3-none-any.whl", hash = "sha256:41aaa7594c7d141b46eed0a58dcd75d20edcc809fdd2c931ecbb4957dc98a892", size = 755132, upload_time = "2025-07-09T14:08:25.533Z" }, + { url = "https://files.pythonhosted.org/packages/d4/12/32c19999a58eec4a695e8ce334442b6135df949f0bb61b2ceaa4fa60d3a9/openai-1.107.1-py3-none-any.whl", hash = "sha256:168f9885b1b70d13ada0868a0d0adfd538c16a02f7fd9fe063851a2c9a025e72", size = 945177, upload_time = "2025-09-10T15:04:37.782Z" }, ] [[package]] From d2cd05eb7a39b9908fe667f5f5f287c0bc78d2dd Mon Sep 17 00:00:00 2001 From: Keepingitneil Date: Fri, 12 Sep 2025 09:12:47 -0700 Subject: [PATCH 8/9] mcp.yaml to gitignore --- .gitignore | 3 ++- mcp.example.yaml | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 mcp.example.yaml diff --git a/.gitignore b/.gitignore index 9c919b90..37b0c5af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .secret +mcp.yaml .gabber/ -.DS_Store \ No newline at end of file +.DS_Store diff --git a/mcp.example.yaml b/mcp.example.yaml new file mode 100644 index 00000000..9a7fb838 --- /dev/null +++ b/mcp.example.yaml @@ -0,0 +1,8 @@ +servers: + - name: blender + transport: + type: datachannel_proxy + local_transport: + type: stdio + command: uvx + args: [blender-mcp] From b9d55a4edf5fe86ada37601c47588af32318d1cd Mon Sep 17 00:00:00 2001 From: Keepingitneil Date: Fri, 12 Sep 2025 09:15:48 -0700 Subject: [PATCH 9/9] bump gabber sdk python --- engine/src/core/mcp/datachannel_transport.py | 4 ++-- engine/src/nodes/core/tool/mcp.py | 11 +++++------ sdks/python/gabber/engine/engine.py | 6 ------ sdks/python/pyproject.toml | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/engine/src/core/mcp/datachannel_transport.py b/engine/src/core/mcp/datachannel_transport.py index aa5cd56c..0084aaf6 100644 --- a/engine/src/core/mcp/datachannel_transport.py +++ b/engine/src/core/mcp/datachannel_transport.py @@ -61,11 +61,11 @@ async def on_message_loop(): session_message = SessionMessage(message) await read_stream_writer.send(session_message) except ValidationError as exc: - logging.error(f"NEIL DC message validation error: {exc}") + logging.error(f"DC message validation error: {exc}") # If JSON parse or model validation fails, send the exception await read_stream_writer.send(exc) except Exception as exc: - logging.error(f"NEIL DC unexpected error: {exc}") + logging.error(f"DC unexpected error: {exc}") await read_stream_writer.send(exc) async def dc_writer(): diff --git a/engine/src/nodes/core/tool/mcp.py b/engine/src/nodes/core/tool/mcp.py index aa18d7d1..3c1b4eec 100644 --- a/engine/src/nodes/core/tool/mcp.py +++ b/engine/src/nodes/core/tool/mcp.py @@ -57,14 +57,14 @@ async def run(self): await asyncio.wait_for(self.session.initialize(), timeout=2) await asyncio.sleep(1) except asyncio.TimeoutError: - logging.error("NEIL MCP Client session timeout") + logging.error("MCP Client session timeout") await exit_stack.aclose() continue try: await self.session_ping_loop(self.session) except Exception as e: - logging.error(f"NEIL MCP Client list_tools error: {e}") + logging.error(f"MCP Client list_tools error: {e}") await exit_stack.aclose() continue @@ -85,7 +85,7 @@ async def create_session(self, exit_stack: contextlib.AsyncExitStack): "Only MCPTransportDatachannelProxy is supported in this node" ) - logging.info(f"NEIL Connecting to MCP server '{mcp_server.name}'") + logging.info(f"Connecting to MCP server '{mcp_server.name}'") read_stream, write_stream = await exit_stack.enter_async_context( mcp.datachannel_host(self.room, "mcp_proxy", mcp_server.name) ) @@ -98,10 +98,9 @@ async def session_ping_loop(self, session: ClientSession): try: while True: await asyncio.sleep(10) - logging.info("NEIL MCP Client sending ping") await asyncio.wait_for(session.send_ping(), timeout=2) except asyncio.CancelledError: - logging.info("NEIL MCP Client ping loop cancelled") + logging.info("MCP Client ping loop cancelled") raise async def to_tool_definitions(self) -> list[runtime_types.ToolDefinition]: @@ -122,7 +121,7 @@ async def to_tool_definitions(self) -> list[runtime_types.ToolDefinition]: return tool_defs async def call_tool(self, tool_call: runtime_types.ToolCall): - logging.info(f"NEIL ************ MCP Client calling tool '{tool_call.name}'") + logging.info(f"MCP Client calling tool '{tool_call.name}'") sess: ClientSession async with self.init_lock: if not self.session: diff --git a/sdks/python/gabber/engine/engine.py b/sdks/python/gabber/engine/engine.py index d65770eb..924f39c6 100644 --- a/sdks/python/gabber/engine/engine.py +++ b/sdks/python/gabber/engine/engine.py @@ -81,7 +81,6 @@ async def connect(self, *, connection_details: types.ConnectionDetails) -> None: await self._livekit_room.connect( connection_details.url, connection_details.token ) - logging.info("NEIL connected to room") while True: if self.connection_state == "connected": @@ -129,16 +128,13 @@ async def subscribe_to_node(self, *, output_or_publish_node: str): async def list_mcp_servers(self) -> List[runtime.MCPServer]: payload = runtime.RuntimeRequestPayloadListMCPServers(type="list_mcp_servers") - logging.info("NEIL Sending list_mcp_servers request") retries = 3 last_exception = None for attempt in range(retries): try: response = await self.runtime_request(payload) if response.type == "list_mcp_servers": - logging.info(f"NEIL list_mcp_servers response: {response}") return response.servers - logging.warning(f"NEIL list_mcp_servers attempt {attempt + 1} failed") except Exception as e: last_exception = e @@ -161,7 +157,6 @@ async def runtime_request( topic=topic, destination_identities=["gabber-engine"], ) - logging.debug(f"NEIL Sent runtime request: {req}") return await asyncio.wait_for(future, timeout=timeout) except Exception as e: future.set_exception(e) @@ -184,7 +179,6 @@ def get_property_pad(self, node_id: str, pad_id: str) -> "PropertyPad": def setup_room_event_listeners(self) -> None: def on_connected(state: rtc.ConnectionState): - logging.info(f"NEIL on connected!!!!!!!!!!!: {state}") self._emit_connection_state_change() def on_participant_connected(participant: rtc.RemoteParticipant): diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index adaf59df..5d1b12ad 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gabber-sdk" -version = "0.2.0" +version = "0.2.1" description = "Python client SDK for interacting with the Gabber Engine" readme = "README.md" requires-python = ">=3.11"