diff --git a/ballerina-interpreter/README.md b/ballerina-interpreter/README.md index 1937029..32ea66d 100644 --- a/ballerina-interpreter/README.md +++ b/ballerina-interpreter/README.md @@ -37,6 +37,75 @@ afmFilePath = "path/to/agent.afm.md" The AFM file path can also be passed as a command-line argument. +## Model Providers + +The model is configured via the `model` block in the AFM frontmatter. Supported providers: + +| `provider` | Required fields | Credentials | +|---|---|---| +| _(omitted)_ | — | `WSO2_MODEL_PROVIDER_TOKEN` env var (WSO2 default model) | +| `wso2` | — | `authentication` (bearer) or `WSO2_MODEL_PROVIDER_TOKEN` | +| `openai` | `name` | `authentication` (api-key) | +| `anthropic` | `name` | `authentication` (api-key) | +| `ollama` | `name` | none (local; optional `url`) | +| `gemini` | `name`, `project` | Google ADC or service account key via `GOOGLE_APPLICATION_CREDENTIALS` | + +### Vertex AI (Gemini) + +`provider: gemini` runs against **Vertex AI**. Vertex mode is selected by the presence of the +`project` field (matching the Python interpreter). No `authentication` block is needed in the +AFM file — credentials are read from the standard `GOOGLE_APPLICATION_CREDENTIALS` environment +variable, which may point at **either** credential format: + +- **Application Default Credentials** (`authorized_user`) — produced by + `gcloud auth application-default login`. The interpreter maps these to the connector's + OAuth2 refresh-token flow. This is the quickest path for local development and is the same + file the Python interpreter uses. +- **Service account key** (`service_account`) — a downloaded JSON key. Recommended for + production / CI. + +```yaml +model: + provider: gemini + name: gemini-2.5-flash # bare names are sent to the "google" publisher + project: your-gcp-project-id + location: us-central1 # optional, defaults to us-central1 +``` + +**Local development (ADC):** + +```bash +gcloud auth application-default login +export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.config/gcloud/application_default_credentials.json" +bal run -- agent-vertex.afm.md +``` + +**Production (service account):** + +```bash +gcloud iam service-accounts create afm-vertex --project YOUR_PROJECT_ID +gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \ + --member="serviceAccount:afm-vertex@YOUR_PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/aiplatform.user" +gcloud iam service-accounts keys create sa-key.json \ + --iam-account=afm-vertex@YOUR_PROJECT_ID.iam.gserviceaccount.com +export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/sa-key.json" +``` + +**Docker** — mount the credentials file (read-only) and point the env var at it inside the +container. Using your ADC file (same as the Python interpreter): + +```bash +docker run -it --rm \ + -v $(pwd)/agent-vertex.afm.md:/app/agent.afm.md \ + -v $HOME/.config/gcloud/application_default_credentials.json:/tmp/adc.json:ro \ + -e GOOGLE_APPLICATION_CREDENTIALS=/tmp/adc.json \ + afm-ballerina-interpreter /app/agent.afm.md +``` + +In a managed environment (e.g. GKE / Cloud Run), prefer an attached service account +(workload identity) instead of mounting a credentials file. + ## Running with Docker ```bash diff --git a/ballerina-interpreter/agent.bal b/ballerina-interpreter/agent.bal index d60e55a..cce76be 100644 --- a/ballerina-interpreter/agent.bal +++ b/ballerina-interpreter/agent.bal @@ -17,11 +17,14 @@ import afm_ballerina.everit.validator; import ballerina/ai; +import ballerina/io; import ballerina/log; import ballerina/os; import ballerina/http; import ballerinax/ai.anthropic; import ballerinax/ai.openai; +import ballerinax/ai.ollama; +import ballerinax/ai.googleapis.vertex; function createAgent(AFMRecord afmRecord, string afmFileDir) returns ai:Agent|error { AFMRecord {metadata, role, instructions} = afmRecord; @@ -127,10 +130,76 @@ function getModel(Model? model) returns ai:ModelProvider|error { model.url ?: "https://api.anthropic.com/v1" ); } + "ollama" => { + string url = model.url ?: "http://localhost:11434"; + return new ollama:ModelProvider(check name.ensureType(), url); + } + "gemini" => { + return getGeminiModel(model, name); + } } return error(string `Model provider: ${provider} not yet supported`); } +// Standard Google environment variable holding the path to a service account JSON key. +const GOOGLE_APP_CREDENTIALS_ENV = "GOOGLE_APPLICATION_CREDENTIALS"; +const DEFAULT_VERTEX_LOCATION = "us-central1"; +//(e.g. "gemini-2.5-flash" -> "google/gemini-2.5-flash"). +const DEFAULT_MODEL_PUBLISHER = "google"; + + +function getGeminiModel(Model model, string name) returns ai:ModelProvider|error { + string? project = model.project; + if project is () { + return error("Vertex AI requires the 'project' field to be set in the model block " + + "(AI Studio Gemini is not yet supported by the Ballerina interpreter)"); + } + + string? credentialsPath = os:getEnv(GOOGLE_APP_CREDENTIALS_ENV); + if credentialsPath is () || credentialsPath.trim() == "" { + return error(string `Vertex AI authentication requires Google credentials. Set the ` + + string `'${GOOGLE_APP_CREDENTIALS_ENV}' environment variable to the path of a service account ` + + string `JSON key file or a gcloud Application Default Credentials file.`); + } + + vertex:VertexAiAuth auth = check resolveVertexAuth(credentialsPath); + + string location = model.location ?: DEFAULT_VERTEX_LOCATION; + string qualifiedModel = name.includes("/") ? name : string `${DEFAULT_MODEL_PUBLISHER}/${name}`; + + vertex:ModelProvider|error vertexModel = new (auth, project, qualifiedModel, location = location); + if vertexModel is error { + return error(string `Failed to initialize the Vertex AI model provider: ${vertexModel.message()}`, vertexModel); + } + return vertexModel; +} + +function resolveVertexAuth(string credentialsPath) returns vertex:VertexAiAuth|error { + json|error credsJson = io:fileReadJson(credentialsPath); + if credsJson is error { + return error(string `Unable to read Google credentials file at '${credentialsPath}': ${credsJson.message()}`, credsJson); + } + + map|error creds = credsJson.ensureType(); + if creds is error { + return error(string `Google credentials file at '${credentialsPath}' is not a valid JSON object`); + } + + if creds["type"] == "authorized_user" { + json clientId = creds["client_id"]; + json clientSecret = creds["client_secret"]; + json refreshToken = creds["refresh_token"]; + if clientId !is string || clientSecret !is string || refreshToken !is string { + return error("Authorized-user (ADC) credentials must contain string 'client_id', " + + "'client_secret', and 'refresh_token' fields"); + } + vertex:OAuth2RefreshConfig oauth2Config = {clientId, clientSecret, refreshToken}; + return oauth2Config; + } + + return credentialsPath; +} + const DEFAULT_SESSION_ID = "sessionId"; isolated function runAgent(ai:Agent agent, json payload, map? inputSchema = (), diff --git a/ballerina-interpreter/tests/agent_test.bal b/ballerina-interpreter/tests/agent_test.bal index c117be2..7b1d477 100644 --- a/ballerina-interpreter/tests/agent_test.bal +++ b/ballerina-interpreter/tests/agent_test.bal @@ -14,9 +14,11 @@ // specific language governing permissions and limitations // under the License. +import ballerina/ai; import ballerina/http; import ballerina/io; import ballerina/lang.runtime; +import ballerina/os; import ballerina/test; @test:Config @@ -410,3 +412,49 @@ function testArrayOutputSchemaInvalidResponse() returns error? { // Should return 500 error due to schema validation failure test:assertEquals(response.statusCode, 500, "Should return 500 for schema validation failure"); } + +@test:Config +function testGetModelGeminiMissingProject() returns error? { + Model model = { + provider: "gemini", + name: "gemini-2.5-flash" + }; + var result = getModel(model); + test:assertTrue(result is error); + test:assertTrue((result).message().includes("'project'")); +} + +@test:Config +function testGetModelGeminiMissingCredentials() returns error? { + check os:unsetEnv(GOOGLE_APP_CREDENTIALS_ENV); + Model model = { + provider: "gemini", + name: "gemini-2.5-flash", + project: "test-project", + location: "us-central1" + }; + var result = getModel(model); + test:assertTrue(result is error); + test:assertTrue((result).message().includes(GOOGLE_APP_CREDENTIALS_ENV)); +} + +@test:Config +function testGetModelOllamaLocalLoopback() returns error? { + Model model = { + provider: "ollama", + name: "llama3" + }; + var result = getModel(model); + test:assertTrue(result is ai:ModelProvider); +} + +@test:Config +function testGetModelOllamaCustomEndpoint() returns error? { + Model model = { + provider: "ollama", + name: "mistral", + url: "http://192.168.1.15:11434" + }; + var result = getModel(model); + test:assertTrue(result is ai:ModelProvider); +} diff --git a/ballerina-interpreter/types.bal b/ballerina-interpreter/types.bal index 392e409..71facc7 100644 --- a/ballerina-interpreter/types.bal +++ b/ballerina-interpreter/types.bal @@ -23,6 +23,8 @@ type Model record {| string name?; string provider?; string url?; + string project?; + string location?; ClientAuthentication authentication?; |}; diff --git a/python-interpreter/packages/afm-core/src/afm/models.py b/python-interpreter/packages/afm-core/src/afm/models.py index 101f6d9..a4a331d 100644 --- a/python-interpreter/packages/afm-core/src/afm/models.py +++ b/python-interpreter/packages/afm-core/src/afm/models.py @@ -57,7 +57,7 @@ def validate_type_fields(self) -> Self: class Model(BaseModel): - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="allow") name: str | None = None provider: str | None = None diff --git a/python-interpreter/packages/afm-langchain/pyproject.toml b/python-interpreter/packages/afm-langchain/pyproject.toml index f6d7ffd..e4b1253 100644 --- a/python-interpreter/packages/afm-langchain/pyproject.toml +++ b/python-interpreter/packages/afm-langchain/pyproject.toml @@ -17,6 +17,8 @@ dependencies = [ "langchain-anthropic>=1.3.1", "mcp>=1.26.0", "langchain-mcp-adapters>=0.2.1", + "langchain-google-genai>=4.2.3", + "langchain-ollama>=1.1.0", ] [project.entry-points."afm.runner"] diff --git a/python-interpreter/packages/afm-langchain/src/afm_langchain/providers.py b/python-interpreter/packages/afm-langchain/src/afm_langchain/providers.py index 32d538b..7248ef2 100644 --- a/python-interpreter/packages/afm-langchain/src/afm_langchain/providers.py +++ b/python-interpreter/packages/afm-langchain/src/afm_langchain/providers.py @@ -27,17 +27,23 @@ if TYPE_CHECKING: from langchain_anthropic import ChatAnthropic from langchain_openai import ChatOpenAI + from langchain_google_genai import ChatGoogleGenerativeAI + from langchain_ollama import ChatOllama DEFAULT_OPENAI_MODEL = "gpt-4o" DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5" +DEFAULT_GEMINI_MODEL = "gemini-2.5-flash" +DEFAULT_OLLAMA_MODEL = "llama3" DEFAULT_OPENAI_URL = "https://api.openai.com/v1" DEFAULT_ANTHROPIC_URL = "https://api.anthropic.com" +DEFAULT_OLLAMA_URL = "http://localhost:11434" # Environment variable names for API keys OPENAI_API_KEY_ENV = "OPENAI_API_KEY" ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY" +GOOGLE_API_KEY_ENV = "GOOGLE_API_KEY" def create_model_provider(afm_model: Model | None = None) -> BaseChatModel: @@ -51,6 +57,10 @@ def create_model_provider(afm_model: Model | None = None) -> BaseChatModel: return _create_openai_model(afm_model) case "anthropic": return _create_anthropic_model(afm_model) + case "gemini": + return _create_gemini_model(afm_model) + case "ollama": + return _create_ollama_model(afm_model) case _: raise ProviderError(f"Unsupported provider: {provider}", provider=provider) @@ -112,6 +122,66 @@ def _create_anthropic_model(afm_model: Model) -> ChatAnthropic: return ChatAnthropic(**kwargs) +def _create_gemini_model(afm_model: Model) -> ChatGoogleGenerativeAI: + try: + from langchain_google_genai import ChatGoogleGenerativeAI + except ImportError as e: + raise ProviderError( + "langchain-google-genai package is required for Gemini models. " + "Install it with: pip install langchain-google-genai", + provider="gemini", + ) from e + + model_name = afm_model.name if afm_model.name else DEFAULT_GEMINI_MODEL + base_url = afm_model.url if afm_model.url else None + + kwargs: dict = { + "model": model_name, + } + + if base_url: + kwargs["base_url"] = base_url + + extra_attrs = afm_model.model_extra or {} + project = extra_attrs.get("project") + location = extra_attrs.get("location") + + if project: + kwargs["project"] = project + kwargs["vertexai"] = True + if location: + kwargs["location"] = location + + if not project or afm_model.authentication: + api_key = _get_api_key( + afm_model.authentication, + GOOGLE_API_KEY_ENV, + "gemini", + ) + if api_key: + kwargs["api_key"] = api_key + + return ChatGoogleGenerativeAI(**kwargs) + +def _create_ollama_model(afm_model: Model) -> ChatOllama: + try: + from langchain_ollama import ChatOllama + except ImportError as e: + raise ProviderError( + "langchain-ollama package is required for Ollama models. " + "Install it with: pip install langchain-ollama", + provider="ollama", + ) from e + + model_name = afm_model.name if afm_model.name else DEFAULT_OLLAMA_MODEL + base_url = afm_model.url if afm_model.url else DEFAULT_OLLAMA_URL + + kwargs: dict = { + "model": model_name, + "base_url": base_url, + } + + return ChatOllama(**kwargs) def _get_api_key( auth: ClientAuthentication | None, diff --git a/python-interpreter/packages/afm-langchain/tests/test_providers.py b/python-interpreter/packages/afm-langchain/tests/test_providers.py new file mode 100644 index 0000000..05652a6 --- /dev/null +++ b/python-interpreter/packages/afm-langchain/tests/test_providers.py @@ -0,0 +1,253 @@ +# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). +# +# WSO2 LLC. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from afm.exceptions import ProviderError +from afm.models import ClientAuthentication, Model +from afm_langchain.providers import create_model_provider + + +class TestOpenAIProvider: + @patch("langchain_openai.ChatOpenAI") + def test_create_openai_with_explicit_credentials(self, mock_chat_openai: MagicMock) -> None: + """Verify OpenAI client creation with credentials passed in AFM metadata.""" + afm_model = Model( + provider="openai", + name="gpt-4o", + authentication=ClientAuthentication( + type="api-key", + api_key="explicit-openai-token-123" + ) + ) + create_model_provider(afm_model) + + mock_chat_openai.assert_called_once() + kwargs = mock_chat_openai.call_args[1] + assert kwargs["model"] == "gpt-4o" + assert kwargs["api_key"] == "explicit-openai-token-123" + + @patch("langchain_openai.ChatOpenAI") + def test_create_openai_with_env_fallback(self, mock_chat_openai: MagicMock) -> None: + """Verify OpenAI client successfully falls back to environment variables.""" + afm_model = Model(provider="openai", name="gpt-4o") + + with patch.dict(os.environ, {"OPENAI_API_KEY": "env-openai-token-456"}): + create_model_provider(afm_model) + + mock_chat_openai.assert_called_once() + kwargs = mock_chat_openai.call_args[1] + assert kwargs["api_key"] == "env-openai-token-456" + + def test_create_openai_missing_credentials_fails(self) -> None: + """Verify ProviderError is raised when no API key can be resolved.""" + afm_model = Model(provider="openai", name="gpt-4o") + + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ProviderError) as exc_info: + create_model_provider(afm_model) + assert "No API key found" in str(exc_info.value) + assert exc_info.value.provider == "openai" + + def test_openai_import_error(self) -> None: + """Verify clean package warning when the underlying langchain library is missing.""" + afm_model = Model( + provider="openai", + name="gpt-4o", + authentication=ClientAuthentication(type="api-key", api_key="dummy") + ) + with patch.dict(sys.modules, {"langchain_openai": None}): + with pytest.raises(ProviderError) as exc_info: + create_model_provider(afm_model) + assert "langchain-openai package is required" in str(exc_info.value) + + +class TestAnthropicProvider: + @patch("langchain_anthropic.ChatAnthropic") + def test_create_anthropic_with_explicit_credentials(self, mock_chat_anthropic: MagicMock) -> None: + """Verify Anthropic client initialization with explicit credentials.""" + afm_model = Model( + provider="anthropic", + name="claude-sonnet-4-5", + authentication=ClientAuthentication( + type="api-key", + api_key="explicit-anthropic-token-123" + ) + ) + create_model_provider(afm_model) + + mock_chat_anthropic.assert_called_once() + kwargs = mock_chat_anthropic.call_args[1] + assert kwargs["model"] == "claude-sonnet-4-5" + assert kwargs["api_key"] == "explicit-anthropic-token-123" + + @patch("langchain_anthropic.ChatAnthropic") + def test_create_anthropic_with_env_fallback(self, mock_chat_anthropic: MagicMock) -> None: + """Verify Anthropic client falls back onto system environment variables.""" + afm_model = Model(provider="anthropic", name="claude-sonnet-4-5") + + with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "env-anthropic-token-456"}): + create_model_provider(afm_model) + + mock_chat_anthropic.assert_called_once() + kwargs = mock_chat_anthropic.call_args[1] + assert kwargs["api_key"] == "env-anthropic-token-456" + + def test_anthropic_import_error(self) -> None: + """Verify clean package warning when langchain-anthropic is missing.""" + afm_model = Model( + provider="anthropic", + name="claude-sonnet-4-5", + authentication=ClientAuthentication(type="api-key", api_key="dummy") + ) + with patch.dict(sys.modules, {"langchain_anthropic": None}): + with pytest.raises(ProviderError) as exc_info: + create_model_provider(afm_model) + assert "langchain-anthropic package is required" in str(exc_info.value) + + +class TestGeminiProvider: + @patch("langchain_google_genai.ChatGoogleGenerativeAI") + def test_create_gemini_studio_explicit_credentials(self, mock_chat_gemini: MagicMock) -> None: + """Verify standard AI Studio pathway accepts and passes API keys cleanly.""" + afm_model = Model( + provider="gemini", + name="gemini-2.5-flash", + authentication=ClientAuthentication( + type="api-key", + api_key="explicit-gemini-token-789" + ) + ) + create_model_provider(afm_model) + + mock_chat_gemini.assert_called_once() + kwargs = mock_chat_gemini.call_args[1] + assert kwargs["model"] == "gemini-2.5-flash" + assert kwargs["api_key"] == "explicit-gemini-token-789" + assert "vertexai" not in kwargs + + @patch("langchain_google_genai.ChatGoogleGenerativeAI") + def test_create_gemini_studio_env_fallback(self, mock_chat_gemini: MagicMock) -> None: + """Verify standard AI Studio pathway falls back onto GOOGLE_API_KEY environment variables.""" + afm_model = Model(provider="gemini", name="gemini-2.5-flash") + + with patch.dict(os.environ, {"GOOGLE_API_KEY": "env-gemini-token-101"}): + create_model_provider(afm_model) + + mock_chat_gemini.assert_called_once() + kwargs = mock_chat_gemini.call_args[1] + assert kwargs["api_key"] == "env-gemini-token-101" + + @patch("langchain_google_genai.ChatGoogleGenerativeAI") + def test_create_gemini_vertex_adc_fallback(self, mock_chat_gemini: MagicMock) -> None: + """Verify enterprise Vertex AI path bypasses API key lookups to allow Application Default Credentials (ADC).""" + afm_model = Model( + provider="gemini", + name="gemini-2.5-flash", + project="wso2-gcp-enterprise-sandbox", + location="us-central1" + ) + create_model_provider(afm_model) + + mock_chat_gemini.assert_called_once() + kwargs = mock_chat_gemini.call_args[1] + assert kwargs["project"] == "wso2-gcp-enterprise-sandbox" + assert kwargs["location"] == "us-central1" + assert kwargs["vertexai"] is True + assert "api_key" not in kwargs + + @patch("langchain_google_genai.ChatGoogleGenerativeAI") + def test_create_gemini_vertex_with_explicit_credentials(self, mock_chat_gemini: MagicMock) -> None: + """Verify enterprise Vertex AI pathways still accept explicit credentials if provided.""" + afm_model = Model( + provider="gemini", + name="gemini-2.5-flash", + project="wso2-gcp-enterprise-sandbox", + location="us-central1", + authentication=ClientAuthentication( + type="api-key", + api_key="vertex-specific-explicit-key" + ) + ) + create_model_provider(afm_model) + + mock_chat_gemini.assert_called_once() + kwargs = mock_chat_gemini.call_args[1] + assert kwargs["project"] == "wso2-gcp-enterprise-sandbox" + assert kwargs["vertexai"] is True + assert kwargs["api_key"] == "vertex-specific-explicit-key" + + def test_gemini_import_error(self) -> None: + """Verify clean package warning when langchain-google-genai is missing.""" + afm_model = Model( + provider="gemini", + name="gemini-2.5-flash", + authentication=ClientAuthentication(type="api-key", api_key="dummy") + ) + with patch.dict(sys.modules, {"langchain_google_genai": None}): + with pytest.raises(ProviderError) as exc_info: + create_model_provider(afm_model) + assert "langchain-google-genai package is required" in str(exc_info.value) + + +class TestOllamaProvider: + @patch("langchain_ollama.ChatOllama") + def test_create_ollama_local_loopback_default(self, mock_chat_ollama: MagicMock) -> None: + """Verify Ollama initialization functions completely unauthenticated with loopback defaults.""" + afm_model = Model(provider="ollama", name="llama3") + create_model_provider(afm_model) + + mock_chat_ollama.assert_called_once() + kwargs = mock_chat_ollama.call_args[1] + assert kwargs["model"] == "llama3" + assert kwargs["base_url"] == "http://localhost:11434" + assert "api_key" not in kwargs + + @patch("langchain_ollama.ChatOllama") + def test_create_ollama_with_custom_endpoint(self, mock_chat_ollama: MagicMock) -> None: + """Verify Ollama correctly respects and binds custom URL parameters (useful for container network links).""" + afm_model = Model( + provider="ollama", + name="mistral", + url="http://host.docker.internal:11434" + ) + create_model_provider(afm_model) + + mock_chat_ollama.assert_called_once() + kwargs = mock_chat_ollama.call_args[1] + assert kwargs["model"] == "mistral" + assert kwargs["base_url"] == "http://host.docker.internal:11434" + + def test_ollama_import_error(self) -> None: + """Verify clean package warning when langchain-ollama is missing.""" + afm_model = Model(provider="ollama", name="llama3") + with patch.dict(sys.modules, {"langchain_ollama": None}): + with pytest.raises(ProviderError) as exc_info: + create_model_provider(afm_model) + assert "langchain-ollama package is required" in str(exc_info.value) + + +class TestGeneralProviderErrors: + def test_unsupported_provider_raises_error(self) -> None: + """Verify ProviderError triggers upon encountering an unknown provider name.""" + afm_model = Model(provider="unsupported-model-ecosystem", name="test-model") + with pytest.raises(ProviderError) as exc_info: + create_model_provider(afm_model) + assert "Unsupported provider: unsupported-model-ecosystem" in str(exc_info.value) \ No newline at end of file diff --git a/python-interpreter/uv.lock b/python-interpreter/uv.lock index 4b0a022..f112be1 100644 --- a/python-interpreter/uv.lock +++ b/python-interpreter/uv.lock @@ -80,7 +80,9 @@ dependencies = [ { name = "afm-core" }, { name = "langchain" }, { name = "langchain-anthropic" }, + { name = "langchain-google-genai" }, { name = "langchain-mcp-adapters" }, + { name = "langchain-ollama" }, { name = "langchain-openai" }, { name = "mcp" }, ] @@ -90,7 +92,9 @@ requires-dist = [ { name = "afm-core", editable = "packages/afm-core" }, { name = "langchain", specifier = ">=1.2.8" }, { name = "langchain-anthropic", specifier = ">=1.3.1" }, + { name = "langchain-google-genai", specifier = ">=4.2.3" }, { name = "langchain-mcp-adapters", specifier = ">=0.2.1" }, + { name = "langchain-ollama", specifier = ">=1.1.0" }, { name = "langchain-openai", specifier = ">=1.1.7" }, { name = "mcp", specifier = ">=1.26.0" }, ] @@ -689,6 +693,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, ] +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -794,6 +807,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "google-auth" +version = "2.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/59/3ed61240ef20b3ae6ed54e82c6f8b6d1f194947bc6679679dd6cdb037594/google_genai-1.75.0.tar.gz", hash = "sha256:56bac3991b311c93f980c0a2abcd287b672146905df1fbd71c92ed633d5a07cf", size = 539039, upload-time = "2026-05-04T22:48:54.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/b6/552d40e96da22921eb1fead7c14b00b5b5473a20e45959488660fab35ee2/google_genai-1.75.0-py3-none-any.whl", hash = "sha256:8dc4c096e7d6288c3087f6893f582fe52468932464781edb8193bd92b9fefb2c", size = 793726, upload-time = "2026-05-04T22:48:53.033Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1033,10 +1085,11 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.2.28" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, + { name = "langchain-protocol" }, { name = "langsmith" }, { name = "packaging" }, { name = "pydantic" }, @@ -1045,9 +1098,24 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/317a1a3ac1df33a64adb3670bf88bbe3b3d5baa274db6863a979db472897/langchain_core-1.2.28.tar.gz", hash = "sha256:271a3d8bd618f795fdeba112b0753980457fc90537c46a0c11998516a74dc2cb", size = 846119, upload-time = "2026-04-08T18:19:34.867Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" }, +] + +[[package]] +name = "langchain-google-genai" +version = "4.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/20/76e3b367d31ee8b8beda715ffdd6a5db12d99700a12936123bd9adaaa00f/langchain_google_genai-4.2.3.tar.gz", hash = "sha256:b36bf2201c7b1f1b5d3e13a122af2f8f829151a15183985dcf24e2bc0fcfe69c", size = 269998, upload-time = "2026-05-21T22:11:34.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/92/32f785f077c7e898da97064f113c73fbd9ad55d1e2169cf3a391b183dedb/langchain_core-1.2.28-py3-none-any.whl", hash = "sha256:80764232581eaf8057bcefa71dbf8adc1f6a28d257ebd8b95ba9b8b452e8c6ac", size = 508727, upload-time = "2026-04-08T18:19:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0f/61f0c667d31bfc80fff5af0985d8d7925a75592283a3d2f4b0abf63614e6/langchain_google_genai-4.2.3-py3-none-any.whl", hash = "sha256:2b1be55443c0f409f52799a95f86011e8fa4d15d50b2ee8db2416096828b7042", size = 68569, upload-time = "2026-05-21T22:11:32.749Z" }, ] [[package]] @@ -1064,6 +1132,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/2f/15d5e6c1765d8404a9cce38d8c81d7b33fb3392f9db5b992c000dddbd2a3/langchain_mcp_adapters-0.2.2-py3-none-any.whl", hash = "sha256:d08e64954e86281002653071b7430e0377c9a577cb4ac3143abfeb3e24ef8797", size = 23288, upload-time = "2026-03-16T17:13:29.073Z" }, ] +[[package]] +name = "langchain-ollama" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ollama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/9b/6641afe8a5bf807e454fd464eddfc7eb2f2df53cb0b29744381171f9c609/langchain_ollama-1.1.0.tar.gz", hash = "sha256:f776f56f6782ae4da7692579b94a6575906118318d1023b455d7207f9d059811", size = 133075, upload-time = "2026-04-07T02:48:00.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/b2/c2acb076590a98bee2816ed5f285e00df162a34238f9e276e175e14ebc35/langchain_ollama-1.1.0-py3-none-any.whl", hash = "sha256:43ac83a6eacb0f43855810739794dd55019e0d9b17bdcf3ecb3b1991ac3b59dd", size = 31413, upload-time = "2026-04-07T02:47:59.642Z" }, +] + [[package]] name = "langchain-openai" version = "1.1.12" @@ -1078,6 +1159,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/a6/68fb22e3604015e6f546fa1d3677d24378b482855ae74710cbf4aec44132/langchain_openai-1.1.12-py3-none-any.whl", hash = "sha256:da71ca3f2d18c16f7a2443cc306aa195ad2a07054335ac9b0626dcae02b6a0c5", size = 88487, upload-time = "2026-03-23T18:59:17.978Z" }, ] +[[package]] +name = "langchain-protocol" +version = "0.0.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" }, +] + [[package]] name = "langgraph" version = "1.1.6" @@ -1473,6 +1566,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "ollama" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/72/5f12423b6b39ca8430fbe56f77fcf4ef60f63067c7c4a2e30e200ed9ec16/ollama-0.6.2.tar.gz", hash = "sha256:936d55daa684f474364c098611c933626f8d6c7d67065c5b7ae0c477b508b07f", size = 53145, upload-time = "2026-04-29T21:21:15.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/d6722beeb2d10f7a3b9ff49375708904fde18f82b5609a0bc4aeb5996a4d/ollama-0.6.2-py3-none-any.whl", hash = "sha256:3ad7daab28e5a973445c36a73882a3ef698c2ebb00e21e308652741577509f7d", size = 15115, upload-time = "2026-04-29T21:21:13.794Z" }, +] + [[package]] name = "openai" version = "2.31.0" @@ -1734,6 +1840,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -2637,6 +2764,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, ] +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + [[package]] name = "xxhash" version = "3.6.0"