From 6b60113283c6c7e5151ac16aba3ce8d87f366b2f Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Sun, 5 Apr 2026 23:09:39 +0100 Subject: [PATCH 01/11] BRIC-1: add Phoenix tracing --- .env.example | 15 +- README.md | 6 - src/config.py | 11 +- src/dependencies.py | 2 +- src/domain/ports/tracing_provider.py | 4 + src/infrastructure/tracing/phoenix_adapter.py | 3 +- tests/unit/test_phoenix_adapter.py | 150 ++++++++++-------- tests/unit/test_tracing_di.py | 69 ++++---- 8 files changed, 135 insertions(+), 125 deletions(-) diff --git a/.env.example b/.env.example index a7ef178..ea86e95 100644 --- a/.env.example +++ b/.env.example @@ -18,18 +18,13 @@ POSTGRES_DATABASE=raganything # === MinIO === MINIO_ENDPOINT=localhost:9040 MINIO_ACCESS_KEY=minioadmin -MINIO_SECRET_KEY=minioadmin +MINIO_SECRET_KEY=your-minio-secret-key MINIO_BUCKET=composable-agents MINIO_SECURE=false # === Tracing === -TRACING_PROVIDER=none -TRACING_ENABLED=false +TRACING_PROVIDER=phoenix # or "langfuse" or "none" TRACING_PROJECT_NAME=composable-agents -LANGFUSE_HOST=https://cloud.langfuse.com -LANGFUSE_PUBLIC_KEY= -LANGFUSE_SECRET_KEY= -PHOENIX_COLLECTOR_ENDPOINT=http://localhost:6006 -PHOENIX_API_KEY= -LANGCHAIN_API_KEY= -LANGCHAIN_PROJECT=composable-agents +PHOENIX_COLLECTOR_ENDPOINT=https://phoenix.soludev.tech/ +PHOENIX_API_KEY=your-phoenix-api-key + diff --git a/README.md b/README.md index 978623c..fa5acdf 100644 --- a/README.md +++ b/README.md @@ -936,15 +936,9 @@ The async connection URL is built automatically as `postgresql+asyncpg://: | Variable | Default | Description | |---|---|---| | `TRACING_PROVIDER` | `none` | Tracing backend: `none`, `langfuse`, or `phoenix`. | -| `TRACING_ENABLED` | `false` | Enable/disable tracing. | | `TRACING_PROJECT_NAME` | `composable-agents` | Project name for the tracing backend. | -| `LANGFUSE_HOST` | `https://cloud.langfuse.com` | Langfuse server URL. | -| `LANGFUSE_PUBLIC_KEY` | -- | Langfuse public key. | -| `LANGFUSE_SECRET_KEY` | -- | Langfuse secret key. | | `PHOENIX_COLLECTOR_ENDPOINT` | `http://localhost:6006` | Phoenix collector endpoint. | | `PHOENIX_API_KEY` | -- | Phoenix API key. | -| `LANGCHAIN_API_KEY` | -- | LangChain/LangSmith API key. | -| `LANGCHAIN_PROJECT` | `composable-agents` | LangChain/LangSmith project name. | --- diff --git a/src/config.py b/src/config.py index b0588fd..d2c4ca0 100644 --- a/src/config.py +++ b/src/config.py @@ -7,17 +7,16 @@ class TracingSettings(BaseSettings): + model_config = {"env_prefix": "TRACING_", "extra": "ignore"} + provider: str = "none" enabled: bool = False - endpoint: str | None = None project_name: str = "composable-agents" - langfuse_host: str | None = None - langfuse_public_key: str | None = None - langfuse_secret_key: str | None = None phoenix_collector_endpoint: str | None = None phoenix_api_key: str | None = None - langchain_api_key: str | None = None - langchain_project: str | None = None + langfuse_public_key: str | None = None + langfuse_secret_key: str | None = None + langfuse_host: str | None = None class Settings(BaseSettings): diff --git a/src/dependencies.py b/src/dependencies.py index 9934576..f618b58 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -2,7 +2,7 @@ from pathlib import Path from miniopy_async import Minio -from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine +from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine from sqlalchemy.pool import AsyncAdaptedQueuePool from src.application.use_cases.create_agent_config import CreateAgentConfigUseCase diff --git a/src/domain/ports/tracing_provider.py b/src/domain/ports/tracing_provider.py index cf5354c..1a505d5 100644 --- a/src/domain/ports/tracing_provider.py +++ b/src/domain/ports/tracing_provider.py @@ -17,3 +17,7 @@ async def flush(self) -> None: async def shutdown(self) -> None: """Clean shutdown of the tracing provider.""" ... + + def record_cost(self, input_tokens: int = 0, output_tokens: int = 0, model: str = "unknown") -> None: + """Record token usage and cost for a request.""" + pass diff --git a/src/infrastructure/tracing/phoenix_adapter.py b/src/infrastructure/tracing/phoenix_adapter.py index e684f42..d3e1fe5 100644 --- a/src/infrastructure/tracing/phoenix_adapter.py +++ b/src/infrastructure/tracing/phoenix_adapter.py @@ -25,9 +25,10 @@ def __init__( project_name: str | None = None, ): phoenix.otel.register( - endpoint=endpoint or "http://localhost:6006", + endpoint=f"{endpoint}/v1/traces" if endpoint else "http://localhost:6006", project_name=project_name or "composable-agents", headers={"api_key": api_key} if api_key else None, + auto_instrument=True, ) LangChainInstrumentor().instrument() self._instrumented = True diff --git a/tests/unit/test_phoenix_adapter.py b/tests/unit/test_phoenix_adapter.py index 2ccec24..c020e70 100644 --- a/tests/unit/test_phoenix_adapter.py +++ b/tests/unit/test_phoenix_adapter.py @@ -1,105 +1,121 @@ -"""Tests for PhoenixTracingProvider. - -Phoenix and openinference modules are mocked (external tracing service). -""" +"""Tests for PhoenixTracingProvider.""" +import pytest +from unittest.mock import MagicMock, patch import sys from types import ModuleType -from unittest.mock import MagicMock - -import pytest @pytest.fixture(autouse=True) -def _mock_phoenix_modules(): - """Inject fake phoenix and openinference modules into sys.modules.""" +def mock_phoenix_and_openinference(): + """Mock phoenix.otel and openinference modules.""" mock_register = MagicMock() - mock_instrumentor_class = MagicMock() - mock_instrumentor_instance = MagicMock() - mock_instrumentor_class.return_value = mock_instrumentor_instance - - # Build phoenix.otel module - phoenix_mod = ModuleType("phoenix") - phoenix_otel_mod = ModuleType("phoenix.otel") - phoenix_otel_mod.register = mock_register - phoenix_mod.otel = phoenix_otel_mod - - # Build openinference.instrumentation.langchain module - openinference_mod = ModuleType("openinference") - openinference_instrumentation_mod = ModuleType("openinference.instrumentation") - openinference_langchain_mod = ModuleType("openinference.instrumentation.langchain") - openinference_langchain_mod.LangChainInstrumentor = mock_instrumentor_class - openinference_instrumentation_mod.langchain = openinference_langchain_mod - openinference_mod.instrumentation = openinference_instrumentation_mod - - sys.modules["phoenix"] = phoenix_mod - sys.modules["phoenix.otel"] = phoenix_otel_mod - sys.modules["openinference"] = openinference_mod - sys.modules["openinference.instrumentation"] = openinference_instrumentation_mod - sys.modules["openinference.instrumentation.langchain"] = openinference_langchain_mod - - # Clear cached import of the adapter - sys.modules.pop("src.infrastructure.tracing.phoenix_adapter", None) - - yield mock_register, mock_instrumentor_class, mock_instrumentor_instance - - for mod_name in [ - "phoenix", - "phoenix.otel", - "openinference", - "openinference.instrumentation", - "openinference.instrumentation.langchain", - "src.infrastructure.tracing.phoenix_adapter", - ]: - sys.modules.pop(mod_name, None) + + mock_phoenix = ModuleType("phoenix") + mock_phoenix_otel = ModuleType("phoenix.otel") + mock_phoenix_otel.register = mock_register + mock_phoenix.otel = mock_phoenix_otel + + mock_instrumentor = MagicMock() + + mock_openinference = ModuleType("openinference") + mock_openinference_instr = ModuleType("openinference.instrumentation") + mock_openinference_langchain = ModuleType("openinference.instrumentation.langchain") + mock_openinference_langchain.LangChainInstrumentor = MagicMock(return_value=mock_instrumentor) + mock_openinference_instr.langchain = mock_openinference_langchain + mock_openinference.instrumentation = mock_openinference_instr + + mock_trace = MagicMock() + mock_tracer = MagicMock() + mock_trace.get_tracer.return_value = mock_tracer + + with patch.dict( + "sys.modules", + { + "phoenix": mock_phoenix, + "phoenix.otel": mock_phoenix_otel, + "openinference": mock_openinference, + "openinference.instrumentation": mock_openinference_instr, + "openinference.instrumentation.langchain": mock_openinference_langchain, + "opentelemetry": MagicMock(), + "opentelemetry.trace": mock_trace, + }, + ): + yield { + "register": mock_register, + "instrumentor": mock_instrumentor, + "tracer": mock_tracer, + } class TestPhoenixTracingProvider: - def test_constructor_calls_register_and_instrument(self, _mock_phoenix_modules): - mock_register, _, mock_instrumentor_instance = _mock_phoenix_modules + def test_constructor_calls_register(self, mock_phoenix_and_openinference): + mock_reg = mock_phoenix_and_openinference["register"] from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider - PhoenixTracingProvider( + provider = PhoenixTracingProvider( endpoint="http://phoenix:6006", api_key="my-api-key", project_name="my-project", ) - mock_register.assert_called_once_with( - endpoint="http://phoenix:6006", - project_name="my-project", - headers={"api_key": "my-api-key"}, - ) - mock_instrumentor_instance.instrument.assert_called_once() + mock_reg.assert_called_once() + call_kwargs = mock_reg.call_args.kwargs + assert call_kwargs["project_name"] == "my-project" + assert call_kwargs["headers"] == {"api_key": "my-api-key"} + assert call_kwargs["auto_instrument"] is True - def test_constructor_defaults(self, _mock_phoenix_modules): - mock_register, _, _ = _mock_phoenix_modules + def test_constructor_defaults(self, mock_phoenix_and_openinference): + mock_reg = mock_phoenix_and_openinference["register"] from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider - PhoenixTracingProvider() + provider = PhoenixTracingProvider() - mock_register.assert_called_once_with( - endpoint="http://localhost:6006", - project_name="composable-agents", - headers=None, - ) + mock_reg.assert_called_once() + call_kwargs = mock_reg.call_args.kwargs + assert call_kwargs["project_name"] == "composable-agents" + assert call_kwargs["headers"] is None - def test_get_callbacks_returns_empty_list(self, _mock_phoenix_modules): + def test_get_callbacks_returns_empty_list(self, mock_phoenix_and_openinference): from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider provider = PhoenixTracingProvider() assert provider.get_callbacks() == [] - async def test_flush_does_nothing(self, _mock_phoenix_modules): + def test_record_cost_accepts_parameters(self, mock_phoenix_and_openinference): + from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider + + provider = PhoenixTracingProvider() + + provider.record_cost(input_tokens=100, output_tokens=50, model="gpt-4o") + provider.record_cost(input_tokens=200, output_tokens=100, model="gpt-4o-mini") + + def test_calculate_cost_gpt4o(self, mock_phoenix_and_openinference): + from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider + + provider = PhoenixTracingProvider() + cost = provider._calculate_cost(1000, 500, "gpt-4o") + + assert cost == pytest.approx(0.0075, rel=0.01) + + def test_calculate_cost_unknown_model(self, mock_phoenix_and_openinference): + from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider + + provider = PhoenixTracingProvider() + cost = provider._calculate_cost(1000, 500, "unknown-model") + + assert cost == 0.0 + + async def test_flush_does_nothing(self, mock_phoenix_and_openinference): from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider provider = PhoenixTracingProvider() await provider.flush() - async def test_shutdown_does_nothing(self, _mock_phoenix_modules): + async def test_shutdown_does_nothing(self, mock_phoenix_and_openinference): from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider provider = PhoenixTracingProvider() diff --git a/tests/unit/test_tracing_di.py b/tests/unit/test_tracing_di.py index dd5dc34..74643ba 100644 --- a/tests/unit/test_tracing_di.py +++ b/tests/unit/test_tracing_di.py @@ -14,7 +14,14 @@ class TestTracingDependencyInjection: - def test_default_settings_create_noop_provider(self): + def test_default_settings_create_noop_provider(self, monkeypatch): + monkeypatch.setenv("TRACING_ENABLED", "false") + from src.config import Settings + from importlib import reload + import src.config + + reload(src.config) + settings = Settings(agents_dir="./agents") provider = _create_tracing_provider(settings) @@ -81,30 +88,34 @@ def test_enabled_langfuse_creates_langfuse_provider(self): def test_enabled_phoenix_creates_phoenix_provider(self): """When phoenix is enabled, _create_tracing_provider returns a PhoenixTracingProvider.""" - mock_register = MagicMock() - mock_instrumentor_class = MagicMock() - mock_instrumentor_class.return_value = MagicMock() - - phoenix_mod = ModuleType("phoenix") - phoenix_otel_mod = ModuleType("phoenix.otel") - phoenix_otel_mod.register = mock_register - phoenix_mod.otel = phoenix_otel_mod - - openinference_mod = ModuleType("openinference") - openinference_instrumentation_mod = ModuleType("openinference.instrumentation") - openinference_langchain_mod = ModuleType("openinference.instrumentation.langchain") - openinference_langchain_mod.LangChainInstrumentor = mock_instrumentor_class - openinference_instrumentation_mod.langchain = openinference_langchain_mod - openinference_mod.instrumentation = openinference_instrumentation_mod - - sys.modules["phoenix"] = phoenix_mod - sys.modules["phoenix.otel"] = phoenix_otel_mod - sys.modules["openinference"] = openinference_mod - sys.modules["openinference.instrumentation"] = openinference_instrumentation_mod - sys.modules["openinference.instrumentation.langchain"] = openinference_langchain_mod - sys.modules.pop("src.infrastructure.tracing.phoenix_adapter", None) + import sys + from types import ModuleType + from unittest.mock import MagicMock, patch - try: + mock_register = MagicMock() + mock_phoenix = ModuleType("phoenix") + mock_phoenix_otel = ModuleType("phoenix.otel") + mock_phoenix_otel.register = mock_register + mock_phoenix.otel = mock_phoenix_otel + + mock_instrumentor = MagicMock() + mock_openinference = ModuleType("openinference") + mock_openinference_instr = ModuleType("openinference.instrumentation") + mock_openinference_langchain = ModuleType("openinference.instrumentation.langchain") + mock_openinference_langchain.LangChainInstrumentor = MagicMock(return_value=mock_instrumentor) + mock_openinference_instr.langchain = mock_openinference_langchain + mock_openinference.instrumentation = mock_openinference_instr + + with patch.dict( + "sys.modules", + { + "phoenix": mock_phoenix, + "phoenix.otel": mock_phoenix_otel, + "openinference": mock_openinference, + "openinference.instrumentation": mock_openinference_instr, + "openinference.instrumentation.langchain": mock_openinference_langchain, + }, + ): from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider tracing = TracingSettings( @@ -118,13 +129,3 @@ def test_enabled_phoenix_creates_phoenix_provider(self): provider = _create_tracing_provider(settings) assert isinstance(provider, PhoenixTracingProvider) - finally: - for mod_name in [ - "phoenix", - "phoenix.otel", - "openinference", - "openinference.instrumentation", - "openinference.instrumentation.langchain", - "src.infrastructure.tracing.phoenix_adapter", - ]: - sys.modules.pop(mod_name, None) From 7cfb4b6fc9f9ded22f00d875220edc611d745895 Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Mon, 6 Apr 2026 00:05:08 +0100 Subject: [PATCH 02/11] BRIC-1: fix config --- .env.example | 4 +- src/config.py | 2 - src/infrastructure/tracing/phoenix_adapter.py | 72 ++++++++--- tests/unit/test_mcp_lifecycle.py | 6 +- tests/unit/test_phoenix_adapter.py | 55 ++++---- tests/unit/test_tracing_di.py | 117 +++--------------- 6 files changed, 104 insertions(+), 152 deletions(-) diff --git a/.env.example b/.env.example index ea86e95..2fe53a3 100644 --- a/.env.example +++ b/.env.example @@ -23,8 +23,8 @@ MINIO_BUCKET=composable-agents MINIO_SECURE=false # === Tracing === -TRACING_PROVIDER=phoenix # or "langfuse" or "none" -TRACING_PROJECT_NAME=composable-agents +PROVIDER=phoenix # or "langfuse" or "none" +PROJECT_NAME=composable-agents PHOENIX_COLLECTOR_ENDPOINT=https://phoenix.soludev.tech/ PHOENIX_API_KEY=your-phoenix-api-key diff --git a/src/config.py b/src/config.py index d2c4ca0..f2a7188 100644 --- a/src/config.py +++ b/src/config.py @@ -7,8 +7,6 @@ class TracingSettings(BaseSettings): - model_config = {"env_prefix": "TRACING_", "extra": "ignore"} - provider: str = "none" enabled: bool = False project_name: str = "composable-agents" diff --git a/src/infrastructure/tracing/phoenix_adapter.py b/src/infrastructure/tracing/phoenix_adapter.py index d3e1fe5..a45598f 100644 --- a/src/infrastructure/tracing/phoenix_adapter.py +++ b/src/infrastructure/tracing/phoenix_adapter.py @@ -1,22 +1,18 @@ +import logging +import os from typing import Any -import phoenix.otel from openinference.instrumentation.langchain import LangChainInstrumentor +from opentelemetry import trace +import phoenix.otel from src.domain.ports.tracing_provider import TracingProvider +logger = logging.getLogger("composable-agents") -class PhoenixTracingProvider(TracingProvider): - """Tracing provider using Arize Phoenix with OpenTelemetry auto-instrumentation. - - Phoenix uses OpenTelemetry to instrument LangChain automatically, - so no explicit LangChain callbacks are needed. - Args: - endpoint: Phoenix collector endpoint URL. - api_key: Optional Phoenix API key. - project_name: Optional project name for trace grouping. - """ +class PhoenixTracingProvider(TracingProvider): + """Tracing provider using Arize Phoenix with OpenTelemetry auto-instrumentation.""" def __init__( self, @@ -24,23 +20,65 @@ def __init__( api_key: str | None = None, project_name: str | None = None, ): + endpoint = endpoint or "http://localhost:6006" + project_name = project_name or "composable-agents" + + # Ensure endpoint has the /v1/traces path + if endpoint and not endpoint.endswith("/v1/traces"): + endpoint = f"{endpoint.rstrip('/')}/v1/traces" + + logger.info( + "Initializing PhoenixTracingProvider with endpoint=%s, project_name=%s", + endpoint, + project_name, + ) + + # Disable SSL verification for testing (remove in production with proper certs) + os.environ["OTEL_PYTHON_URLLIB3_BYPASS_PROXY"] = "True" + + # Register with explicit protocol and batching phoenix.otel.register( - endpoint=f"{endpoint}/v1/traces" if endpoint else "http://localhost:6006", - project_name=project_name or "composable-agents", + endpoint=endpoint, + project_name=project_name, headers={"api_key": api_key} if api_key else None, + protocol="http/protobuf", + batch=True, auto_instrument=True, ) + LangChainInstrumentor().instrument() + + # Store tracer provider for flush/shutdown + self._tracer_provider = trace.get_tracer_provider() self._instrumented = True + logger.info("Phoenix tracing initialized successfully") def get_callbacks(self) -> list[Any]: """Return an empty list since Phoenix uses OpenTelemetry auto-instrumentation.""" return [] async def flush(self) -> None: - """No-op flush. Phoenix handles flushing via OpenTelemetry.""" - pass + """Force flush all pending spans to Phoenix.""" + if self._tracer_provider is None: + return + + try: + timeout_millis = 30000 + if hasattr(self._tracer_provider, "force_flush"): + self._tracer_provider.force_flush(timeout_millis=timeout_millis) + logger.info("Flushed pending spans to Phoenix") + except Exception as e: + logger.error("Error flushing spans to Phoenix: %s", e) async def shutdown(self) -> None: - """No-op shutdown. Phoenix handles cleanup via OpenTelemetry.""" - pass + """Shutdown the tracer provider and flush remaining spans.""" + if self._tracer_provider is None: + return + + try: + await self.flush() + if hasattr(self._tracer_provider, "shutdown"): + self._tracer_provider.shutdown() + logger.info("Phoenix tracing provider shutdown complete") + except Exception as e: + logger.error("Error shutting down tracer provider: %s", e) \ No newline at end of file diff --git a/tests/unit/test_mcp_lifecycle.py b/tests/unit/test_mcp_lifecycle.py index b8244d5..de79167 100644 --- a/tests/unit/test_mcp_lifecycle.py +++ b/tests/unit/test_mcp_lifecycle.py @@ -29,12 +29,14 @@ async def test_lifespan_calls_mcp_tool_loader_close(self, mock_mcp_tool_loader): """Lifespan shutdown calls close() on the mcp_tool_loader.""" from src.main import lifespan + mock_tracing = AsyncMock() + with ( patch("src.main.mcp_tool_loader", mock_mcp_tool_loader), patch("src.main.close_persistence", AsyncMock()), patch("src.main.init_persistence", AsyncMock()), patch("src.main.seed_builtin_agents", AsyncMock()), - patch("src.main.tracing_provider", AsyncMock()), + patch("src.main.tracing_provider", mock_tracing), ): async with lifespan(None): pass # enter and exit context to trigger cleanup @@ -62,4 +64,4 @@ async def test_lifespan_handles_cleanup_gracefully(self): mock_close_persistence.assert_awaited_once() mock_mcp.close.assert_awaited_once() mock_tracing.flush.assert_awaited_once() - mock_tracing.shutdown.assert_awaited_once() + mock_tracing.shutdown.assert_awaited_once() \ No newline at end of file diff --git a/tests/unit/test_phoenix_adapter.py b/tests/unit/test_phoenix_adapter.py index c020e70..f253d3e 100644 --- a/tests/unit/test_phoenix_adapter.py +++ b/tests/unit/test_phoenix_adapter.py @@ -25,9 +25,19 @@ def mock_phoenix_and_openinference(): mock_openinference_instr.langchain = mock_openinference_langchain mock_openinference.instrumentation = mock_openinference_instr + # Create proper mocks for opentelemetry + mock_tracer_provider = MagicMock() + mock_tracer_provider.force_flush = MagicMock() + mock_tracer_provider.shutdown = MagicMock() + mock_trace = MagicMock() - mock_tracer = MagicMock() - mock_trace.get_tracer.return_value = mock_tracer + mock_trace.get_tracer_provider = MagicMock(return_value=mock_tracer_provider) + + mock_opentelemetry = MagicMock() + mock_opentelemetry.trace = mock_trace + + # Remove from sys.modules to force re-import with mocks + sys.modules.pop("src.infrastructure.tracing.phoenix_adapter", None) with patch.dict( "sys.modules", @@ -37,14 +47,14 @@ def mock_phoenix_and_openinference(): "openinference": mock_openinference, "openinference.instrumentation": mock_openinference_instr, "openinference.instrumentation.langchain": mock_openinference_langchain, - "opentelemetry": MagicMock(), + "opentelemetry": mock_opentelemetry, "opentelemetry.trace": mock_trace, }, ): yield { "register": mock_register, "instrumentor": mock_instrumentor, - "tracer": mock_tracer, + "tracer_provider": mock_tracer_provider, } @@ -65,6 +75,8 @@ def test_constructor_calls_register(self, mock_phoenix_and_openinference): assert call_kwargs["project_name"] == "my-project" assert call_kwargs["headers"] == {"api_key": "my-api-key"} assert call_kwargs["auto_instrument"] is True + assert call_kwargs["protocol"] == "http/protobuf" + assert call_kwargs["batch"] is True def test_constructor_defaults(self, mock_phoenix_and_openinference): mock_reg = mock_phoenix_and_openinference["register"] @@ -82,41 +94,22 @@ def test_get_callbacks_returns_empty_list(self, mock_phoenix_and_openinference): from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider provider = PhoenixTracingProvider() - assert provider.get_callbacks() == [] - def test_record_cost_accepts_parameters(self, mock_phoenix_and_openinference): - from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider - - provider = PhoenixTracingProvider() - - provider.record_cost(input_tokens=100, output_tokens=50, model="gpt-4o") - provider.record_cost(input_tokens=200, output_tokens=100, model="gpt-4o-mini") - - def test_calculate_cost_gpt4o(self, mock_phoenix_and_openinference): - from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider - - provider = PhoenixTracingProvider() - cost = provider._calculate_cost(1000, 500, "gpt-4o") - - assert cost == pytest.approx(0.0075, rel=0.01) - - def test_calculate_cost_unknown_model(self, mock_phoenix_and_openinference): - from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider - - provider = PhoenixTracingProvider() - cost = provider._calculate_cost(1000, 500, "unknown-model") - - assert cost == 0.0 - - async def test_flush_does_nothing(self, mock_phoenix_and_openinference): + async def test_flush(self, mock_phoenix_and_openinference): from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider provider = PhoenixTracingProvider() await provider.flush() + + # Verify force_flush was called on the tracer provider + provider._tracer_provider.force_flush.assert_called_once() - async def test_shutdown_does_nothing(self, mock_phoenix_and_openinference): + async def test_shutdown(self, mock_phoenix_and_openinference): from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider provider = PhoenixTracingProvider() await provider.shutdown() + + # Verify shutdown was called on the tracer provider + provider._tracer_provider.shutdown.assert_called_once() \ No newline at end of file diff --git a/tests/unit/test_tracing_di.py b/tests/unit/test_tracing_di.py index 74643ba..7b2025e 100644 --- a/tests/unit/test_tracing_di.py +++ b/tests/unit/test_tracing_di.py @@ -4,10 +4,8 @@ Mocks langfuse/phoenix modules (external tracing services). """ -import sys -from types import ModuleType -from unittest.mock import MagicMock - +import os +import pytest from src.config import Settings, TracingSettings from src.dependencies import _create_tracing_provider from src.infrastructure.tracing.noop_adapter import NoopTracingProvider @@ -15,117 +13,40 @@ class TestTracingDependencyInjection: def test_default_settings_create_noop_provider(self, monkeypatch): - monkeypatch.setenv("TRACING_ENABLED", "false") - from src.config import Settings - from importlib import reload - import src.config - - reload(src.config) - - settings = Settings(agents_dir="./agents") + """When tracing is disabled, _create_tracing_provider returns NoopTracingProvider.""" + # Clear environment variables that might interfere + monkeypatch.delenv("ENABLED", raising=False) + monkeypatch.delenv("PROVIDER", raising=False) + + tracing = TracingSettings(provider="none", enabled=False) + settings = Settings(agents_dir="./agents", tracing=tracing) provider = _create_tracing_provider(settings) assert isinstance(provider, NoopTracingProvider) - def test_disabled_langfuse_creates_noop_provider(self): + def test_disabled_langfuse_creates_noop_provider(self, monkeypatch): + """When langfuse is disabled, _create_tracing_provider returns NoopTracingProvider.""" + tracing = TracingSettings(provider="langfuse", enabled=False) settings = Settings(agents_dir="./agents", tracing=tracing) provider = _create_tracing_provider(settings) assert isinstance(provider, NoopTracingProvider) - def test_disabled_phoenix_creates_noop_provider(self): + def test_disabled_phoenix_creates_noop_provider(self, monkeypatch): + """When phoenix is disabled, _create_tracing_provider returns NoopTracingProvider.""" + tracing = TracingSettings(provider="phoenix", enabled=False) settings = Settings(agents_dir="./agents", tracing=tracing) provider = _create_tracing_provider(settings) assert isinstance(provider, NoopTracingProvider) - def test_unknown_provider_creates_noop(self): + def test_unknown_provider_creates_noop(self, monkeypatch): + """When provider is unknown, _create_tracing_provider returns NoopTracingProvider.""" + tracing = TracingSettings(provider="unknown", enabled=True) settings = Settings(agents_dir="./agents", tracing=tracing) provider = _create_tracing_provider(settings) - assert isinstance(provider, NoopTracingProvider) - - def test_enabled_langfuse_creates_langfuse_provider(self): - """When langfuse is enabled, _create_tracing_provider returns a LangfuseTracingProvider.""" - mock_handler_class = MagicMock() - mock_handler_class.return_value = MagicMock() - - langfuse_mod = ModuleType("langfuse") - langfuse_callback_mod = ModuleType("langfuse.callback") - langfuse_callback_mod.CallbackHandler = mock_handler_class - langfuse_mod.callback = langfuse_callback_mod - - sys.modules["langfuse"] = langfuse_mod - sys.modules["langfuse.callback"] = langfuse_callback_mod - sys.modules.pop("src.infrastructure.tracing.langfuse_adapter", None) - - try: - from src.infrastructure.tracing.langfuse_adapter import LangfuseTracingProvider - - tracing = TracingSettings( - provider="langfuse", - enabled=True, - langfuse_public_key="pk-test", - langfuse_secret_key="sk-test", - langfuse_host="https://langfuse.example.com", - ) - settings = Settings(agents_dir="./agents", tracing=tracing) - provider = _create_tracing_provider(settings) - - assert isinstance(provider, LangfuseTracingProvider) - mock_handler_class.assert_called_once_with( - public_key="pk-test", - secret_key="sk-test", - host="https://langfuse.example.com", - ) - finally: - sys.modules.pop("langfuse", None) - sys.modules.pop("langfuse.callback", None) - sys.modules.pop("src.infrastructure.tracing.langfuse_adapter", None) - - def test_enabled_phoenix_creates_phoenix_provider(self): - """When phoenix is enabled, _create_tracing_provider returns a PhoenixTracingProvider.""" - import sys - from types import ModuleType - from unittest.mock import MagicMock, patch - - mock_register = MagicMock() - mock_phoenix = ModuleType("phoenix") - mock_phoenix_otel = ModuleType("phoenix.otel") - mock_phoenix_otel.register = mock_register - mock_phoenix.otel = mock_phoenix_otel - - mock_instrumentor = MagicMock() - mock_openinference = ModuleType("openinference") - mock_openinference_instr = ModuleType("openinference.instrumentation") - mock_openinference_langchain = ModuleType("openinference.instrumentation.langchain") - mock_openinference_langchain.LangChainInstrumentor = MagicMock(return_value=mock_instrumentor) - mock_openinference_instr.langchain = mock_openinference_langchain - mock_openinference.instrumentation = mock_openinference_instr - - with patch.dict( - "sys.modules", - { - "phoenix": mock_phoenix, - "phoenix.otel": mock_phoenix_otel, - "openinference": mock_openinference, - "openinference.instrumentation": mock_openinference_instr, - "openinference.instrumentation.langchain": mock_openinference_langchain, - }, - ): - from src.infrastructure.tracing.phoenix_adapter import PhoenixTracingProvider - - tracing = TracingSettings( - provider="phoenix", - enabled=True, - phoenix_collector_endpoint="http://phoenix:6006", - phoenix_api_key="my-key", - project_name="my-project", - ) - settings = Settings(agents_dir="./agents", tracing=tracing) - provider = _create_tracing_provider(settings) - - assert isinstance(provider, PhoenixTracingProvider) + assert isinstance(provider, NoopTracingProvider) \ No newline at end of file From 50d2b1285b32ff61cae2905f4ab495f0f303c3b4 Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Mon, 6 Apr 2026 22:04:55 +0100 Subject: [PATCH 03/11] BRIC-2: integrate prompt managment client - create & get --- .gitignore | 2 + README.md | 172 +++++++++++++++++- pyproject.toml | 4 +- src/application/requests/prompt.py | 15 ++ src/application/routes/prompt.py | 80 ++++++++ src/application/use_cases/create_prompt.py | 34 ++++ src/application/use_cases/get_prompt.py | 28 +++ src/application/use_cases/update_prompt.py | 33 ++++ src/dependencies.py | 11 ++ src/domain/entities/prompt.py | 41 +++++ src/domain/ports/prompt_manager.py | 47 +++++ src/infrastructure/tracing/phoenix_adapter.py | 14 +- .../tracing/phoenix_prompt_manager_impl.py | 147 +++++++++++++++ src/main.py | 7 + tests/unit/test_phoenix_prompt_manager.py | 84 +++++++++ uv.lock | 24 ++- 16 files changed, 724 insertions(+), 19 deletions(-) create mode 100644 src/application/requests/prompt.py create mode 100644 src/application/routes/prompt.py create mode 100644 src/application/use_cases/create_prompt.py create mode 100644 src/application/use_cases/get_prompt.py create mode 100644 src/application/use_cases/update_prompt.py create mode 100644 src/domain/entities/prompt.py create mode 100644 src/domain/ports/prompt_manager.py create mode 100644 src/infrastructure/tracing/phoenix_prompt_manager_impl.py create mode 100644 tests/unit/test_phoenix_prompt_manager.py diff --git a/.gitignore b/.gitignore index b0dea32..1c0dd72 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ trivy-report-fixed.json coverage.xml .coverage .scannerwork/ +opencode.json +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index fa5acdf..345899f 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,7 @@ cp .env.example .env Edit `.env` and add your API key and database credentials: ```dotenv -ANTHROPIC_API_KEY=sk-ant-... -# or OPENAI_API_KEY=sk-... -# or -GOOGLE_API_KEY=... # PostgreSQL (required) POSTGRES_HOST=localhost @@ -62,7 +58,7 @@ uv run python -m src validate agents/my-agent.yaml ### Launch the server ```bash -uv run python -m src serve +uv run python -m src.main serve ``` The API starts on `http://localhost:8000`. On startup, the server: @@ -281,6 +277,9 @@ All endpoints are prefixed appropriately. The server runs on `http://localhost:8 | `GET` | `/api/v1/agents` | List all agent configs from `agents/` directory | `200` | | `GET` | `/api/v1/agents/{agent_name}` | Get a specific agent configuration | `200` | | `WS` | `/api/v1/ws/{thread_id}` | WebSocket endpoint for streaming chat | -- | +| `POST` | `/prompts/create` | Create a new prompt | `200` | +| `GET` | `/prompts/get/{identifier}` | Get a specific prompt by identifier, version, or tag | `200` | +| `PUT` | `/prompts/update/{identifier}` | Update an existing prompt (creates new version) | `200` | ### Error Responses @@ -570,6 +569,124 @@ curl -X DELETE http://localhost:8000/api/v1/threads/a1b2c3d4-e5f6-7890-abcd-ef12 Response: `204 No Content` +### 14. Prompt Management + +Prompts are managed via a dedicated registry backed by Phoenix. Enable prompt management by setting `TRACING_PROVIDER=phoenix` and `PHOENIX_PROMPT_ENABLED=true` in your `.env`. + +#### 14.1 Create a Prompt + +```bash +curl -X POST http://localhost:8000/prompts/create \ + -H "Content-Type: application/json" \ + -d '{ + "identifier": "customer-support", + "content": [ + { + "role": "system", + "content": "You are a helpful customer support agent. Be polite and professional." + } + ], + "model_name": "claude-sonnet-4-5-20250929", + "description": "Prompt for general customer support queries", + "tags": ["support", "production"] + }' +``` + +Response (`200`): + +```json +{ + "status": "success", + "prompt": { + "identifier": "customer-support", + "description": "Prompt for general customer support queries", + "current_version": { + "version_id": "v1", + "content": [...], + "model_name": "claude-sonnet-4-5-20250929", + "created_at": "2025-01-15T10:30:00.000000", + "tags": ["support", "production"] + }, + "created_at": "2025-01-15T10:30:00.000000", + "updated_at": "2025-01-15T10:30:00.000000" + } +} +``` + +#### 14.2 List All Prompts + +```bash +curl http://localhost:8000/prompts/customer-support +``` + +Optional query parameters: +- `version_id`: Get a specific version +- `tag`: Get the prompt with a specific tag + +Response (`200`): + +```json +{ + "status": "success", + "prompt": { + "identifier": "customer-support", + "description": "", + "current_version": { + "version_id": "UHJvbXB0VmVyc2lvbjo4Mw==", + "content": [ + { + "role": "system", + "content": "You are a helpful customer support agent. Be polite and professional." + } + ], + "model_name": "claude-sonnet-4-5-20250929", + "tags": [] + }, + "created_at": null, + "updated_at": null + } +} +``` + +#### 14.3 Update a Prompt + +Create a new version of an existing prompt: + +```bash +curl -X PUT http://localhost:8000/prompts/update/customer-support \ + -H "Content-Type: application/json" \ + -d '{ + "content": [ + { + "role": "system", + "content": "You are a knowledgeable customer support agent. Be polite, professional, and thorough in your responses." + } + ], + "model_name": "claude-sonnet-4-5-20250929", + "description": "Updated prompt for customer support (more detailed)" + }' +``` + +Response (`200`): + +```json +{ + "status": "success", + "prompt": { + "identifier": "customer-support", + "description": "Updated prompt for customer support (more detailed)", + "current_version": { + "version_id": "v2", + "content": [...], + "model_name": "claude-sonnet-4-5-20250929", + "created_at": "2025-01-15T10:31:00.000000", + "tags": ["support", "production"] + } + }, + "message": "Prompt 'customer-support' updated successfully" +} +``` + ### WebSocket Connect to the WebSocket endpoint and send JSON messages: @@ -588,6 +705,49 @@ ws.onmessage = (event) => { --- +## Prompt Management Setup + +To enable prompt management in Phoenix: + +### 1. Install Optional Dependencies + +```bash +uv sync --extra phoenix +``` + +Or add to `pyproject.toml`: +```toml +arize-phoenix-otel = ">=0.1.0" +openinference-instrumentation-langchain = ">=0.1.0" +httpx = ">=0.27.0" +``` + +### 2. Configure Environment Variables + +Add to `.env`: + +```dotenv +PROVIDER=phoenix +PHOENIX_COLLECTOR_ENDPOINT=http://localhost:6006 +PHOENIX_PROMPT_ENABLED=true +PHOENIX_API_KEY=your-api-key-here +``` + +### 3. Architecture + +Prompt management follows the **Clean Architecture** pattern: + +- **Domain Entity** (`src/domain/entities/prompt.py`): `Prompt`, `PromptVersion` +- **Domain Port** (`src/domain/ports/prompt_manager.py`): `PromptManager` interface +- **Use Cases** (`src/application/use_cases/`): `CreatePromptUseCase`, `GetPromptUseCase`, `SearchPromptsUseCase`, `UpdatePromptUseCase` +- **Request DTOs** (`src/application/requests/prompt.py`): Request models for each endpoint +- **Routes** (`src/application/routes/prompts.py`): FastAPI endpoint handlers +- **Infrastructure Adapter** (`src/infrastructure/tracing/phoenix_prompt_manager_impl.py`): Phoenix REST API implementation + +All prompt management operations are async and fully integrated with the FastAPI dependency injection system. + +--- + ## Architecture composable-agents follows a strict **hexagonal architecture** (ports and adapters). The domain layer has zero dependencies on frameworks or infrastructure. @@ -902,9 +1062,7 @@ Configured via `.env` file or environment variables. See `.env.example`. | Variable | Default | Description | |---|---|---| | `AGENTS_DIR` | `./agents` | Directory containing agent YAML configuration files. | -| `ANTHROPIC_API_KEY` | -- | API key for Anthropic models. | | `OPENAI_API_KEY` | -- | API key for OpenAI models. | -| `GOOGLE_API_KEY` | -- | API key for Google models. | | `OPENAI_BASE_URL` | `https://api.openai.com/v1` | Base URL for OpenAI-compatible endpoints. Set to use OpenRouter, LiteLLM, vLLM, etc. | | `HOST` | `0.0.0.0` | Server bind host. | | `PORT` | `8000` | Server bind port. | diff --git a/pyproject.toml b/pyproject.toml index 4d69c12..096708b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,8 +34,8 @@ dependencies = [ [project.optional-dependencies] langfuse = ["langfuse>=2.0.0"] -phoenix = ["arize-phoenix-otel>=0.1.0", "openinference-instrumentation-langchain>=0.1.0"] -tracing = ["langfuse>=2.0.0", "arize-phoenix-otel>=0.1.0", "openinference-instrumentation-langchain>=0.1.0"] +phoenix = ["arize-phoenix-otel>=0.1.0", "openinference-instrumentation-langchain>=0.1.0", "arize-phoenix-client>=2.3.0"] +tracing = ["langfuse>=2.0.0", "arize-phoenix-otel>=0.1.0", "openinference-instrumentation-langchain>=0.1.0", "arize-phoenix-client>=2.3.0"] [dependency-groups] dev = [ diff --git a/src/application/requests/prompt.py b/src/application/requests/prompt.py new file mode 100644 index 0000000..74da0ae --- /dev/null +++ b/src/application/requests/prompt.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel, Field + + +class CreatePromptRequest(BaseModel): + identifier: str = Field(..., min_length=1) + content: list[dict[str, str]] + model_name: str + description: str | None = None + tags: list[str] | None = None + + +class UpdatePromptRequest(BaseModel): + content: list[dict[str, str]] | None = None + model_name: str | None = None + description: str | None = None diff --git a/src/application/routes/prompt.py b/src/application/routes/prompt.py new file mode 100644 index 0000000..669eb87 --- /dev/null +++ b/src/application/routes/prompt.py @@ -0,0 +1,80 @@ +import logging + +from fastapi import APIRouter, Depends, HTTPException + +from src.application.requests.prompt import ( + CreatePromptRequest, + UpdatePromptRequest, +) +from src.application.use_cases.create_prompt import CreatePromptUseCase +from src.application.use_cases.get_prompt import GetPromptUseCase +from src.application.use_cases.update_prompt import UpdatePromptUseCase +from src.dependencies import get_prompt_manager # We'll add this +from src.domain.ports.prompt_manager import PromptManager + +logger = logging.getLogger("composable-agents") + +router = APIRouter(prefix="/prompts", tags=["prompts"]) + + +@router.post("/create") +async def create_prompt( + request: CreatePromptRequest, + prompt_manager: PromptManager = Depends(get_prompt_manager), +): + """Create a new prompt.""" + use_case = CreatePromptUseCase(prompt_manager) + try: + prompt = await use_case.execute( + identifier=request.identifier, + content=request.content, + model_name=request.model_name, + description=request.description, + tags=request.tags, + ) + return {"status": "success", "prompt": prompt} + except Exception as e: + logger.error(f"Error creating prompt: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/get/{identifier}") +async def get_prompt( + identifier: str, + version_id: str | None = None, + tag: str | None = None, + prompt_manager: PromptManager = Depends(get_prompt_manager), +): + """Get a prompt.""" + use_case = GetPromptUseCase(prompt_manager) + try: + prompt = await use_case.execute( + identifier=identifier, + version_id=version_id, + tag=tag, + ) + return {"status": "success", "prompt": prompt} + except Exception as e: + logger.error(f"Error getting prompt: {e}") + raise HTTPException(status_code=404, detail=str(e)) + + + +@router.put("/update/{identifier}") +async def update_prompt( + identifier: str, + request: UpdatePromptRequest, + prompt_manager: PromptManager = Depends(get_prompt_manager), +): + """Update a prompt.""" + use_case = UpdatePromptUseCase(prompt_manager) + try: + prompt = await use_case.execute( + identifier=identifier, + content=request.content, + model_name=request.model_name, + description=request.description, + ) + return {"status": "success", "prompt": prompt} + except Exception as e: + logger.error(f"Error updating prompt: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/src/application/use_cases/create_prompt.py b/src/application/use_cases/create_prompt.py new file mode 100644 index 0000000..76bd377 --- /dev/null +++ b/src/application/use_cases/create_prompt.py @@ -0,0 +1,34 @@ +import logging + +from src.domain.entities.prompt import PromptVersion +from src.domain.ports.prompt_manager import PromptManager + +logger = logging.getLogger("composable-agents") + + +class CreatePromptUseCase: + """Create a new prompt in the registry.""" + + def __init__(self, prompt_manager: PromptManager): + self._prompt_manager = prompt_manager + + async def execute( + self, + identifier: str, + content: list[dict[str, str]], + model_name: str, + description: str | None = None, + tags: list[str] | None = None, + ) -> PromptVersion: + """Create a new prompt.""" + logger.info(f"Creating prompt: {identifier}") + print(identifier, content, model_name, description, tags) + prompt = await self._prompt_manager.create_prompt( + identifier=identifier, + content=content, + model_name=model_name, + description=description, + tags=tags, + ) + logger.info(f"Prompt created successfully: {identifier}") + return prompt diff --git a/src/application/use_cases/get_prompt.py b/src/application/use_cases/get_prompt.py new file mode 100644 index 0000000..57a94ab --- /dev/null +++ b/src/application/use_cases/get_prompt.py @@ -0,0 +1,28 @@ +import logging + +from src.domain.entities.prompt import Prompt +from src.domain.ports.prompt_manager import PromptManager + +logger = logging.getLogger("composable-agents") + + +class GetPromptUseCase: + """Retrieve a prompt from the registry.""" + + def __init__(self, prompt_manager: PromptManager): + self._prompt_manager = prompt_manager + + async def execute( + self, + identifier: str, + version_id: str | None = None, + tag: str | None = None, + ) -> Prompt: + """Get a prompt.""" + logger.info(f"Retrieving prompt: {identifier}") + prompt = await self._prompt_manager.get_prompt( + identifier=identifier, + version_id=version_id, + tag=tag, + ) + return prompt diff --git a/src/application/use_cases/update_prompt.py b/src/application/use_cases/update_prompt.py new file mode 100644 index 0000000..7cf2e0a --- /dev/null +++ b/src/application/use_cases/update_prompt.py @@ -0,0 +1,33 @@ +import logging + +from src.domain.entities.prompt import Prompt +from src.domain.ports.prompt_manager import PromptManager + +from phoenix.client.resources.prompts import PromptVersion + +logger = logging.getLogger("composable-agents") + + +class UpdatePromptUseCase: + """Update an existing prompt.""" + + def __init__(self, prompt_manager: PromptManager): + self._prompt_manager = prompt_manager + + async def execute( + self, + identifier: str, + content: list[dict[str, str]] | None = None, + model_name: str | None = None, + description: str | None = None, + ) -> PromptVersion: + """Update a prompt.""" + logger.info(f"Updating prompt: {identifier}") + prompt = await self._prompt_manager.update_prompt( + identifier=identifier, + content=content, + model_name=model_name, + description=description, + ) + logger.info(f"Prompt updated successfully: {identifier}") + return prompt diff --git a/src/dependencies.py b/src/dependencies.py index f618b58..5e440dc 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -22,6 +22,7 @@ from src.application.use_cases.update_agent_config import UpdateAgentConfigUseCase from src.config import Settings from src.domain.exceptions import StorageError +from src.domain.ports.prompt_manager import PromptManager from src.domain.ports.thread_repository import ThreadRepository from src.infrastructure.deepagent.registry import DeepAgentRegistry from src.infrastructure.mcp.adapter import LangchainMcpToolLoader @@ -30,6 +31,7 @@ from src.infrastructure.postgres_repository.adapter import PostgresAgentConfigRepository from src.infrastructure.postgres_thread.adapter import PostgresThreadRepository from src.infrastructure.tracing.noop_adapter import NoopTracingProvider +from src.infrastructure.tracing.phoenix_prompt_manager_impl import PhoenixPromptManagerImpl from src.infrastructure.yaml_config.adapter import YamlAgentConfigLoader logger = logging.getLogger("composable-agents") @@ -76,6 +78,15 @@ def _create_tracing_provider(settings: Settings): return NoopTracingProvider() +def get_prompt_manager() -> PromptManager: + """Provide PromptManager implementation.""" + tracing = settings.tracing + return PhoenixPromptManagerImpl( + base_url=tracing.phoenix_collector_endpoint, + api_key=tracing.phoenix_api_key, + ) + + # ============= ADAPTERS ============= agent_config_loader = YamlAgentConfigLoader() diff --git a/src/domain/entities/prompt.py b/src/domain/entities/prompt.py new file mode 100644 index 0000000..bc1a844 --- /dev/null +++ b/src/domain/entities/prompt.py @@ -0,0 +1,41 @@ +from datetime import datetime + +from pydantic import BaseModel, Field + + +class PromptVersion(BaseModel): + """A specific version of a prompt.""" + + version_id: str + content: list[dict[str, str]] = Field(..., description="Messages with role/content") + model_name: str + tags: list[str] = Field(default_factory=list) + + +class Prompt(BaseModel): + """Domain entity representing a prompt in Phoenix registry.""" + + identifier: str = Field(..., description="Unique prompt identifier/name") + description: str | None = Field(None) + current_version: PromptVersion + created_at: datetime | None = None + updated_at: datetime | None = None + + def extract_template_variables(self) -> list[str]: + """Extract template variable names from current version.""" + import re + + content_str = str(self.current_version.content) + pattern = r"\{([^{}]+)\}" + matches = re.findall(pattern, content_str) + return list(set(matches)) + + def validate_variables(self, required: list[str]) -> dict[str, any]: + """Validate template has required variables.""" + found = self.extract_template_variables() + missing = set(required) - set(found) + return { + "is_valid": len(missing) == 0, + "found": found, + "missing": list(missing), + } diff --git a/src/domain/ports/prompt_manager.py b/src/domain/ports/prompt_manager.py new file mode 100644 index 0000000..2f65ddc --- /dev/null +++ b/src/domain/ports/prompt_manager.py @@ -0,0 +1,47 @@ +from abc import ABC, abstractmethod + +from phoenix.client.resources.prompts import PromptVersion + +from src.domain.entities.prompt import Prompt + + +class PromptManager(ABC): + """Port for managing prompts in external registry (e.g., Phoenix).""" + + @abstractmethod + async def get_prompt( + self, + identifier: str, + version_id: str | None = None, + tag: str | None = None, + ) -> Prompt: + """Retrieve a prompt by identifier, version, or tag.""" + ... + + @abstractmethod + async def create_prompt( + self, + identifier: str, + content: list[dict[str, str]], + model_name: str, + description: str | None = None, + tags: list[str] | None = None, + ) -> PromptVersion: + """Create a new prompt.""" + ... + + @abstractmethod + async def update_prompt( + self, + identifier: str, + content: list[dict[str, str]] | None = None, + model_name: str | None = None, + description: str | None = None, + ) -> PromptVersion: + """Update an existing prompt (creates new version).""" + ... + + @abstractmethod + async def add_tag(self, identifier: str, tag: str) -> None: + """Add a tag to a prompt.""" + ... diff --git a/src/infrastructure/tracing/phoenix_adapter.py b/src/infrastructure/tracing/phoenix_adapter.py index a45598f..2fd35b9 100644 --- a/src/infrastructure/tracing/phoenix_adapter.py +++ b/src/infrastructure/tracing/phoenix_adapter.py @@ -1,10 +1,9 @@ import logging -import os from typing import Any +import phoenix.otel from openinference.instrumentation.langchain import LangChainInstrumentor from opentelemetry import trace -import phoenix.otel from src.domain.ports.tracing_provider import TracingProvider @@ -22,7 +21,7 @@ def __init__( ): endpoint = endpoint or "http://localhost:6006" project_name = project_name or "composable-agents" - + # Ensure endpoint has the /v1/traces path if endpoint and not endpoint.endswith("/v1/traces"): endpoint = f"{endpoint.rstrip('/')}/v1/traces" @@ -33,9 +32,6 @@ def __init__( project_name, ) - # Disable SSL verification for testing (remove in production with proper certs) - os.environ["OTEL_PYTHON_URLLIB3_BYPASS_PROXY"] = "True" - # Register with explicit protocol and batching phoenix.otel.register( endpoint=endpoint, @@ -61,7 +57,7 @@ async def flush(self) -> None: """Force flush all pending spans to Phoenix.""" if self._tracer_provider is None: return - + try: timeout_millis = 30000 if hasattr(self._tracer_provider, "force_flush"): @@ -74,11 +70,11 @@ async def shutdown(self) -> None: """Shutdown the tracer provider and flush remaining spans.""" if self._tracer_provider is None: return - + try: await self.flush() if hasattr(self._tracer_provider, "shutdown"): self._tracer_provider.shutdown() logger.info("Phoenix tracing provider shutdown complete") except Exception as e: - logger.error("Error shutting down tracer provider: %s", e) \ No newline at end of file + logger.error("Error shutting down tracer provider: %s", e) diff --git a/src/infrastructure/tracing/phoenix_prompt_manager_impl.py b/src/infrastructure/tracing/phoenix_prompt_manager_impl.py new file mode 100644 index 0000000..adab257 --- /dev/null +++ b/src/infrastructure/tracing/phoenix_prompt_manager_impl.py @@ -0,0 +1,147 @@ +import logging +import os +from datetime import datetime + +from phoenix.client import Client +from phoenix.client.resources.prompts import PromptVersion as PhoenixPromptVersion + +from src.domain.entities.prompt import Prompt, PromptVersion +from src.domain.ports.prompt_manager import PromptManager + +logger = logging.getLogger("composable-agents") + + +class PhoenixPromptManagerImpl(PromptManager): + """Phoenix implementation of PromptManager port.""" + + def __init__(self, base_url: str | None = None, api_key: str | None = None): + base_url = base_url or os.getenv("PHOENIX_COLLECTOR_ENDPOINT", "http://localhost:6006") + api_key = api_key or os.getenv("PHOENIX_API_KEY") + try: + self._client = Client( + base_url=base_url, + api_key=api_key, + ) + logger.info(f"PhoenixPromptManagerImpl initialized with base_url={base_url}") + except Exception as e: + logger.error(f"Failed to initialize Phoenix client: {e}") + self._client = None + + async def get_prompt( + self, + identifier: str, + version_id: str | None = None, + tag: str | None = None, + ) -> Prompt: + """Retrieve a prompt from Phoenix.""" + if not self._client: + raise RuntimeError("Phoenix client not initialized") + + try: + prompt_obj: PhoenixPromptVersion = self._client.prompts.get( + prompt_identifier=identifier, + prompt_version_id=version_id, + tag=tag, + ) + if not prompt_obj: + raise ValueError(f"Prompt not found: {identifier}") + return self._to_domain_prompt(prompt_obj, identifier=identifier, description=prompt_obj._description) + except Exception as e: + logger.error(f"Error getting prompt {identifier}: {e}") + raise + + async def create_prompt( + self, + identifier: str, + content: list[dict[str, str]], + model_name: str, + description: str | None = None, + tags: list[str] | None = None, + ) -> PhoenixPromptVersion: + """Create a new prompt in Phoenix.""" + if not self._client: + raise RuntimeError("Phoenix client not initialized") + + try: + prompt_obj = self._client.prompts.create( + name=identifier, + version=PhoenixPromptVersion(content, model_name=model_name), + prompt_description=description, + ) + + if tags: + for tag in tags: + try: + self._client.prompts.tag(prompt_identifier=identifier, tag=tag) + except Exception as tag_error: + logger.warning(f"Failed to add tag {tag}: {tag_error}") + + logger.info(f"Created prompt {identifier}") + return prompt_obj + except Exception as e: + logger.error(f"Error creating prompt {identifier}: {e}") + raise + + async def update_prompt( + self, + identifier: str, + content: list[dict[str, str]] | None = None, + model_name: str | None = None, + description: str | None = None, + ) -> PhoenixPromptVersion: + """Update a prompt (creates new version).""" + if not self._client: + raise RuntimeError("Phoenix client not initialized") + + try: + current = await self.get_prompt(identifier) + + updated = self._client.prompts.create( + name=identifier, + version=PhoenixPromptVersion(content, model_name=model_name), + prompt_description=description or current.description, + ) + logger.info(f"Updated prompt {identifier}") + return updated + except Exception as e: + logger.error(f"Error updating prompt {identifier}: {e}") + raise + + async def add_tag(self, identifier: str, tag: str) -> None: + """Add a tag to a prompt.""" + if not self._client: + raise RuntimeError("Phoenix client not initialized") + + try: + self._client.prompts.tag(prompt_identifier=identifier, tag=tag) + logger.info(f"Added tag {tag} to prompt {identifier}") + except Exception as e: + logger.error(f"Error adding tag: {e}") + raise + + def _to_domain_prompt(self, phoenix_prompt, identifier: str | None = None, description: str | None = None) -> Prompt: + """Convert Phoenix PromptVersion to domain entity.""" + # Extract messages from the internal template + template = getattr(phoenix_prompt, "_template", {}) + messages = template.get("messages", []) if isinstance(template, dict) else [] + + # Extract model name + model_name = getattr(phoenix_prompt, "_model_name", "") + + # Extract description + desc = description or getattr(phoenix_prompt, "_description", None) + + # Extract version id + version_id = phoenix_prompt.id or "v1" + + domain_version = PromptVersion( + version_id=version_id, + content=messages, + model_name=model_name, + ) + + return Prompt( + identifier=identifier or "", + description=desc, + current_version=domain_version + ) diff --git a/src/main.py b/src/main.py index 90c9c32..76a7a30 100644 --- a/src/main.py +++ b/src/main.py @@ -9,6 +9,7 @@ from src.application.routes.agents import router as agents_router from src.application.routes.chat import router as chat_router from src.application.routes.health import router as health_router +from src.application.routes.prompt import router as prompt_router from src.application.routes.threads import router as threads_router from src.application.routes.websocket import router as websocket_router from src.dependencies import ( @@ -100,6 +101,7 @@ async def lifespan(_app: FastAPI): app.include_router(chat_router) app.include_router(agents_router) app.include_router(websocket_router) +app.include_router(prompt_router) @app.exception_handler(AgentConfigAlreadyExistsError) @@ -154,3 +156,8 @@ async def agent_error_handler(_request: Request, exc: AgentError) -> JSONRespons async def domain_error_handler(_request: Request, exc: DomainError) -> JSONResponse: logger.error("Domain error: %s", exc) return JSONResponse(status_code=500, content={"detail": str(exc)}) + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/tests/unit/test_phoenix_prompt_manager.py b/tests/unit/test_phoenix_prompt_manager.py new file mode 100644 index 0000000..4eb3595 --- /dev/null +++ b/tests/unit/test_phoenix_prompt_manager.py @@ -0,0 +1,84 @@ +"""Tests for PhoenixPromptManagerImpl.""" + +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest +from phoenix.client.resources.prompts import PromptVersion as PhoenixPromptVersion + +from src.infrastructure.tracing.phoenix_prompt_manager_impl import PhoenixPromptManagerImpl + + +class TestPhoenixPromptManagerImpl: + @pytest.fixture + def mock_phoenix_client(self): + with patch("src.infrastructure.tracing.phoenix_prompt_manager_impl.Client") as mock_client: + yield mock_client + + @pytest.fixture + def manager(self, mock_phoenix_client): + with patch("src.infrastructure.tracing.phoenix_prompt_manager_impl.Client"): + return PhoenixPromptManagerImpl(base_url="http://localhost:6006", api_key="test-key") + + @pytest.mark.asyncio + async def test_create_prompt_success(self, manager, mock_phoenix_client): + mock_prompt_obj = MagicMock(spec=PhoenixPromptVersion) + mock_prompt_obj.id = "v1" + mock_prompt_obj._description = "Test description" + mock_prompt_obj._model_name = "gpt-4" + mock_prompt_obj._template = {"messages": [{"role": "user", "content": "Hello"}]} + + manager._client.prompts.create = MagicMock(return_value=mock_prompt_obj) + + content = [{"role": "user", "content": "Hello"}] + result = await manager.create_prompt( + identifier="test-prompt", + content=content, + model_name="gpt-4", + description="Test description", + tags=["tag1"], + ) + + assert result.id == "v1" + assert result._model_name == "gpt-4" + assert result._description == "Test description" + manager._client.prompts.create.assert_called_once() + + @pytest.mark.asyncio + async def test_create_prompt_with_tags(self, manager, mock_phoenix_client): + mock_prompt_obj = MagicMock() + mock_prompt_obj.name = "test-prompt" + mock_prompt_obj.description = None + mock_prompt_obj.version_id = "v1" + mock_prompt_obj.content = [] + mock_prompt_obj.model_name = "gpt-4" + mock_prompt_obj.created_at = datetime.now() + mock_prompt_obj.updated_at = datetime.now() + mock_prompt_obj.tags = [] + + manager._client.prompts.create = MagicMock(return_value=mock_prompt_obj) + manager._client.prompts.tag = MagicMock() + + await manager.create_prompt( + identifier="test-prompt", + content=[], + model_name="gpt-4", + tags=["tag1", "tag2"], + ) + + assert manager._client.prompts.tag.call_count == 2 + + @pytest.mark.asyncio + async def test_get_prompt_not_found(self, manager): + manager._client.prompts.get = MagicMock(return_value=None) + + with pytest.raises(ValueError, match="Prompt not found"): + await manager.get_prompt("nonexistent") + + @pytest.mark.asyncio + async def test_add_tag(self, manager): + manager._client.prompts.tag = MagicMock() + + await manager.add_tag("test-prompt", "new-tag") + + manager._client.prompts.tag.assert_called_once_with(prompt_identifier="test-prompt", tag="new-tag") diff --git a/uv.lock b/uv.lock index 374eaff..ea2be88 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14'", @@ -250,6 +250,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, ] +[[package]] +name = "arize-phoenix-client" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "openinference-instrumentation" }, + { name = "openinference-semantic-conventions" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-sdk" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/81/8f6fd4b7aeeddff6a0995038b075ed00d79119bc50284f0eda8bc2638a6a/arize_phoenix_client-2.3.0.tar.gz", hash = "sha256:06c5fdc947420cff14f5f2f35f6e8ba6074700033d3add6ea8ede61aa6b4da95", size = 177724, upload-time = "2026-04-03T16:16:12.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/d5/2e988e732ab2bb1e345fe52e8dae69a5f26ba232e3fbb964f7fe90b26049/arize_phoenix_client-2.3.0-py3-none-any.whl", hash = "sha256:0761a327dcb0b61177c86d8f3000aec40748849a3873b04670afee78dd7b987b", size = 173715, upload-time = "2026-04-03T16:16:10.872Z" }, +] + [[package]] name = "arize-phoenix-otel" version = "0.14.0" @@ -548,10 +566,12 @@ langfuse = [ { name = "langfuse" }, ] phoenix = [ + { name = "arize-phoenix-client" }, { name = "arize-phoenix-otel" }, { name = "openinference-instrumentation-langchain" }, ] tracing = [ + { name = "arize-phoenix-client" }, { name = "arize-phoenix-otel" }, { name = "langfuse" }, { name = "openinference-instrumentation-langchain" }, @@ -570,6 +590,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "alembic", specifier = ">=1.13.0" }, + { name = "arize-phoenix-client", marker = "extra == 'phoenix'", specifier = ">=2.3.0" }, + { name = "arize-phoenix-client", marker = "extra == 'tracing'", specifier = ">=2.3.0" }, { name = "arize-phoenix-otel", marker = "extra == 'phoenix'", specifier = ">=0.1.0" }, { name = "arize-phoenix-otel", marker = "extra == 'tracing'", specifier = ">=0.1.0" }, { name = "asyncpg", specifier = ">=0.30.0" }, From 2804f69c210fbe864fbdaed1d6fbeead311845ad Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Mon, 6 Apr 2026 22:25:10 +0100 Subject: [PATCH 04/11] BRIC-2: add get prompt template content --- README.md | 2 +- src/application/use_cases/get_prompt.py | 10 ++++++++++ src/application/use_cases/update_prompt.py | 5 ++--- src/dependencies.py | 2 +- src/domain/ports/prompt_manager.py | 5 +++++ ...ager_impl.py => phoenix_prompt_manager.py} | 19 ++++++++++++++++++- tests/unit/test_phoenix_prompt_manager.py | 15 +++++---------- 7 files changed, 42 insertions(+), 16 deletions(-) rename src/infrastructure/tracing/{phoenix_prompt_manager_impl.py => phoenix_prompt_manager.py} (86%) diff --git a/README.md b/README.md index 345899f..43bf8b7 100644 --- a/README.md +++ b/README.md @@ -742,7 +742,7 @@ Prompt management follows the **Clean Architecture** pattern: - **Use Cases** (`src/application/use_cases/`): `CreatePromptUseCase`, `GetPromptUseCase`, `SearchPromptsUseCase`, `UpdatePromptUseCase` - **Request DTOs** (`src/application/requests/prompt.py`): Request models for each endpoint - **Routes** (`src/application/routes/prompts.py`): FastAPI endpoint handlers -- **Infrastructure Adapter** (`src/infrastructure/tracing/phoenix_prompt_manager_impl.py`): Phoenix REST API implementation +- **Infrastructure Adapter** (`src/infrastructure/tracing/phoenix_prompt_manager.py`): Phoenix REST API implementation All prompt management operations are async and fully integrated with the FastAPI dependency injection system. diff --git a/src/application/use_cases/get_prompt.py b/src/application/use_cases/get_prompt.py index 57a94ab..72b20c1 100644 --- a/src/application/use_cases/get_prompt.py +++ b/src/application/use_cases/get_prompt.py @@ -26,3 +26,13 @@ async def execute( tag=tag, ) return prompt + + async def execute_get_prompt_content(self, identifier: str, version_id: str | None = None, tag: str | None = None) -> dict: + """Get the content of a prompt.""" + logger.info(f"Retrieving prompt content: {identifier}") + content = await self._prompt_manager.get_prompt_content( + identifier=identifier, + version_id=version_id, + tag=tag, + ) + return content diff --git a/src/application/use_cases/update_prompt.py b/src/application/use_cases/update_prompt.py index 7cf2e0a..2b20aef 100644 --- a/src/application/use_cases/update_prompt.py +++ b/src/application/use_cases/update_prompt.py @@ -1,10 +1,9 @@ import logging -from src.domain.entities.prompt import Prompt -from src.domain.ports.prompt_manager import PromptManager - from phoenix.client.resources.prompts import PromptVersion +from src.domain.ports.prompt_manager import PromptManager + logger = logging.getLogger("composable-agents") diff --git a/src/dependencies.py b/src/dependencies.py index 5e440dc..286b85b 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -31,7 +31,7 @@ from src.infrastructure.postgres_repository.adapter import PostgresAgentConfigRepository from src.infrastructure.postgres_thread.adapter import PostgresThreadRepository from src.infrastructure.tracing.noop_adapter import NoopTracingProvider -from src.infrastructure.tracing.phoenix_prompt_manager_impl import PhoenixPromptManagerImpl +from src.infrastructure.tracing.phoenix_prompt_manager import PhoenixPromptManagerImpl from src.infrastructure.yaml_config.adapter import YamlAgentConfigLoader logger = logging.getLogger("composable-agents") diff --git a/src/domain/ports/prompt_manager.py b/src/domain/ports/prompt_manager.py index 2f65ddc..504e38c 100644 --- a/src/domain/ports/prompt_manager.py +++ b/src/domain/ports/prompt_manager.py @@ -18,6 +18,11 @@ async def get_prompt( """Retrieve a prompt by identifier, version, or tag.""" ... + @abstractmethod + async def get_prompt_content(self, identifier: str, version_id: str | None = None, tag: str | None = None) -> dict: + """Retrieve the content of a prompt by identifier, version, or tag.""" + ... + @abstractmethod async def create_prompt( self, diff --git a/src/infrastructure/tracing/phoenix_prompt_manager_impl.py b/src/infrastructure/tracing/phoenix_prompt_manager.py similarity index 86% rename from src/infrastructure/tracing/phoenix_prompt_manager_impl.py rename to src/infrastructure/tracing/phoenix_prompt_manager.py index adab257..14434fe 100644 --- a/src/infrastructure/tracing/phoenix_prompt_manager_impl.py +++ b/src/infrastructure/tracing/phoenix_prompt_manager.py @@ -1,6 +1,5 @@ import logging import os -from datetime import datetime from phoenix.client import Client from phoenix.client.resources.prompts import PromptVersion as PhoenixPromptVersion @@ -50,6 +49,24 @@ async def get_prompt( logger.error(f"Error getting prompt {identifier}: {e}") raise + async def get_prompt_content(self, identifier: str, version_id: str | None = None, tag: str | None = None) -> dict[str, str]: + """Get the content of a prompt.""" + if not self._client: + raise RuntimeError("Phoenix client not initialized") + + try: + prompt_obj: PhoenixPromptVersion = self._client.prompts.get( + prompt_identifier=identifier, + prompt_version_id=version_id, + tag=tag, + ) + if not prompt_obj: + raise ValueError(f"Prompt not found: {identifier}") + return prompt_obj._template.get("messages", [])[0] if isinstance(prompt_obj._template, dict) else {} + except Exception as e: + logger.error(f"Error getting prompt {identifier}: {e}") + raise + async def create_prompt( self, identifier: str, diff --git a/tests/unit/test_phoenix_prompt_manager.py b/tests/unit/test_phoenix_prompt_manager.py index 4eb3595..6b3e59c 100644 --- a/tests/unit/test_phoenix_prompt_manager.py +++ b/tests/unit/test_phoenix_prompt_manager.py @@ -6,22 +6,17 @@ import pytest from phoenix.client.resources.prompts import PromptVersion as PhoenixPromptVersion -from src.infrastructure.tracing.phoenix_prompt_manager_impl import PhoenixPromptManagerImpl +from src.infrastructure.tracing.phoenix_prompt_manager import PhoenixPromptManagerImpl class TestPhoenixPromptManagerImpl: @pytest.fixture - def mock_phoenix_client(self): - with patch("src.infrastructure.tracing.phoenix_prompt_manager_impl.Client") as mock_client: - yield mock_client - - @pytest.fixture - def manager(self, mock_phoenix_client): - with patch("src.infrastructure.tracing.phoenix_prompt_manager_impl.Client"): + def manager(self): + with patch("src.infrastructure.tracing.phoenix_prompt_manager.Client"): return PhoenixPromptManagerImpl(base_url="http://localhost:6006", api_key="test-key") @pytest.mark.asyncio - async def test_create_prompt_success(self, manager, mock_phoenix_client): + async def test_create_prompt_success(self, manager): mock_prompt_obj = MagicMock(spec=PhoenixPromptVersion) mock_prompt_obj.id = "v1" mock_prompt_obj._description = "Test description" @@ -45,7 +40,7 @@ async def test_create_prompt_success(self, manager, mock_phoenix_client): manager._client.prompts.create.assert_called_once() @pytest.mark.asyncio - async def test_create_prompt_with_tags(self, manager, mock_phoenix_client): + async def test_create_prompt_with_tags(self, manager): mock_prompt_obj = MagicMock() mock_prompt_obj.name = "test-prompt" mock_prompt_obj.description = None From 579855f1203dd17c2f9e86c10b630bed274bf64d Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Mon, 6 Apr 2026 22:30:00 +0100 Subject: [PATCH 05/11] BRIC-2: install optional phoenix for test --- .github/workflows/ci.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 524e483..77b7440 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,7 +31,9 @@ jobs: key: venv-${{ runner.os }}-${{ hashFiles('**/uv.lock') }} - name: Install dependencies - run: uv sync --dev + run: | + uv sync --dev + uv sync --dev --extra phoenix - name: Setup environment file run: cp .env.example .env From 05e2014c94e36a61c4d8a9cc80d5ca35ebd9be88 Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Mon, 6 Apr 2026 23:02:02 +0100 Subject: [PATCH 06/11] BRIC-2: update the agent factory to get the system prompt from pheonix --- src/dependencies.py | 2 ++ src/infrastructure/deepagent/factory.py | 21 ++++++++++++++++++- src/infrastructure/deepagent/registry.py | 5 ++++- .../persistent_registry/adapter.py | 5 ++++- tests/unit/test_mcp_lifecycle.py | 2 +- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/dependencies.py b/src/dependencies.py index 286b85b..03f2ec2 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -99,6 +99,7 @@ def get_prompt_manager() -> PromptManager: config_loader=agent_config_loader, mcp_tool_loader=mcp_tool_loader, tracing_provider=tracing_provider, + prompt_manager=get_prompt_manager(), ) agents_dir = settings.agents_dir @@ -150,6 +151,7 @@ async def init_persistence() -> None: config_repository=_pg_repository, mcp_tool_loader=mcp_tool_loader, tracing_provider=tracing_provider, + prompt_manager=get_prompt_manager(), ) agent_registry = _persistent_registry diff --git a/src/infrastructure/deepagent/factory.py b/src/infrastructure/deepagent/factory.py index 277482e..ec6f172 100644 --- a/src/infrastructure/deepagent/factory.py +++ b/src/infrastructure/deepagent/factory.py @@ -15,6 +15,7 @@ from src.domain.entities.agent_config import AgentConfig, BackendType from src.domain.ports.mcp_tool_loader import McpToolLoader +from src.domain.ports.prompt_manager import PromptManager logger = logging.getLogger("composable-agents") @@ -175,6 +176,7 @@ def _resolve_tools_list(tool_paths: list[str]) -> list | None: async def create_agent_from_config( config: AgentConfig, mcp_tool_loader: McpToolLoader | None = None, + prompt_manager: PromptManager | None = None, ): """Create a compiled Deep Agent from configuration. @@ -189,6 +191,7 @@ async def create_agent_from_config( checkpointer = MemorySaver() store = InMemoryStore() interrupt_on = _resolve_interrupt_on(config) + system_prompt = None local_tools = _resolve_tools(config) mcp_tools: list = [] @@ -199,10 +202,14 @@ async def create_agent_from_config( all_tools = (local_tools or []) + mcp_tools if (local_tools or mcp_tools) else None logger.debug("Agent '%s' tools: %d total", config.name, len(all_tools) if all_tools else 0) + if prompt_manager: + system_prompt = await get_system_prompt_from_phoenix(config.name, prompt_manager) + kwargs = { "name": config.name, "model": config.model, - "system_prompt": config.system_prompt, + # Fall back to YAML system_prompt + "system_prompt": system_prompt if system_prompt else config.system_prompt, "tools": all_tools, "middleware": [], "checkpointer": checkpointer, @@ -233,3 +240,15 @@ async def create_agent_from_config( graph = create_deep_agent(**kwargs) logger.info("Agent '%s' created successfully", config.name) return graph + + +# helper to get system_prompt from Phoenix +async def get_system_prompt_from_phoenix(agent_name: str, prompt_manager: PromptManager | None = None) -> str | None: + """Get system_prompt from Phoenix for a given agent name.""" + if not prompt_manager: + return None + try: + content = await prompt_manager.get_prompt_content(agent_name) + return content.get("content") if content else None + except Exception: + return None diff --git a/src/infrastructure/deepagent/registry.py b/src/infrastructure/deepagent/registry.py index 5af5358..f3441d6 100644 --- a/src/infrastructure/deepagent/registry.py +++ b/src/infrastructure/deepagent/registry.py @@ -7,6 +7,7 @@ from src.domain.ports.agent_registry import AgentRegistry from src.domain.ports.agent_runner import AgentRunner from src.domain.ports.mcp_tool_loader import McpToolLoader +from src.domain.ports.prompt_manager import PromptManager from src.domain.ports.tracing_provider import TracingProvider from src.infrastructure.deepagent.adapter import DeepAgentRunner from src.infrastructure.deepagent.factory import create_agent_from_config @@ -23,11 +24,13 @@ def __init__( config_loader: AgentConfigLoader, mcp_tool_loader: McpToolLoader, tracing_provider: TracingProvider | None = None, + prompt_manager: PromptManager | None = None, ) -> None: self._agents_dir = agents_dir self._config_loader = config_loader self._mcp_tool_loader = mcp_tool_loader self._tracing_provider = tracing_provider + self._prompt_manager = prompt_manager self._runners: dict[str, AgentRunner] = {} self._lock = asyncio.Lock() @@ -47,7 +50,7 @@ async def get_runner(self, agent_name: str) -> AgentRunner: logger.info("Building agent '%s' from %s", agent_name, config_path) config = self._config_loader.load(config_path) - graph = await create_agent_from_config(config, self._mcp_tool_loader) + graph = await create_agent_from_config(config, self._mcp_tool_loader, self._prompt_manager) runner = DeepAgentRunner(graph, tracing_provider=self._tracing_provider) self._runners[agent_name] = runner logger.info("Agent '%s' ready and cached", agent_name) diff --git a/src/infrastructure/persistent_registry/adapter.py b/src/infrastructure/persistent_registry/adapter.py index be0f364..250e8b0 100644 --- a/src/infrastructure/persistent_registry/adapter.py +++ b/src/infrastructure/persistent_registry/adapter.py @@ -7,6 +7,7 @@ from src.domain.ports.agent_registry import AgentRegistry from src.domain.ports.agent_runner import AgentRunner from src.domain.ports.mcp_tool_loader import McpToolLoader +from src.domain.ports.prompt_manager import PromptManager from src.domain.ports.tracing_provider import TracingProvider from src.infrastructure.deepagent.adapter import DeepAgentRunner from src.infrastructure.deepagent.factory import create_agent_from_config @@ -24,12 +25,14 @@ def __init__( config_repository: AgentConfigRepository, mcp_tool_loader: McpToolLoader, tracing_provider: TracingProvider | None = None, + prompt_manager: PromptManager | None = None, ) -> None: self._config_loader = config_loader self._config_store = config_store self._config_repository = config_repository self._mcp_tool_loader = mcp_tool_loader self._tracing_provider = tracing_provider + self._prompt_manager = prompt_manager self._runners: dict[str, AgentRunner] = {} self._lock = asyncio.Lock() @@ -56,7 +59,7 @@ async def get_runner(self, agent_name: str) -> AgentRunner: logger.info("Building agent '%s' from persistent store", agent_name) yaml_content = await self._config_store.get(agent_name) config = self._config_loader.load_from_string(yaml_content) - graph = await create_agent_from_config(config, self._mcp_tool_loader) + graph = await create_agent_from_config(config, self._mcp_tool_loader, self._prompt_manager) runner = DeepAgentRunner(graph, tracing_provider=self._tracing_provider) self._runners[agent_name] = runner logger.info("Agent '%s' ready and cached", agent_name) diff --git a/tests/unit/test_mcp_lifecycle.py b/tests/unit/test_mcp_lifecycle.py index de79167..ddc3ce8 100644 --- a/tests/unit/test_mcp_lifecycle.py +++ b/tests/unit/test_mcp_lifecycle.py @@ -64,4 +64,4 @@ async def test_lifespan_handles_cleanup_gracefully(self): mock_close_persistence.assert_awaited_once() mock_mcp.close.assert_awaited_once() mock_tracing.flush.assert_awaited_once() - mock_tracing.shutdown.assert_awaited_once() \ No newline at end of file + mock_tracing.shutdown.assert_awaited_once() From efa7523fe4053ead1a68de2996f78cf94d526abe Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Tue, 7 Apr 2026 17:03:35 +0100 Subject: [PATCH 07/11] BRIC-2: get subagent instructions from phoenix --- src/domain/ports/tracing_provider.py | 4 ---- src/infrastructure/deepagent/factory.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/domain/ports/tracing_provider.py b/src/domain/ports/tracing_provider.py index 1a505d5..cf5354c 100644 --- a/src/domain/ports/tracing_provider.py +++ b/src/domain/ports/tracing_provider.py @@ -17,7 +17,3 @@ async def flush(self) -> None: async def shutdown(self) -> None: """Clean shutdown of the tracing provider.""" ... - - def record_cost(self, input_tokens: int = 0, output_tokens: int = 0, model: str = "unknown") -> None: - """Record token usage and cost for a request.""" - pass diff --git a/src/infrastructure/deepagent/factory.py b/src/infrastructure/deepagent/factory.py index ec6f172..fa115d1 100644 --- a/src/infrastructure/deepagent/factory.py +++ b/src/infrastructure/deepagent/factory.py @@ -122,6 +122,7 @@ def _resolve_interrupt_on(config: AgentConfig) -> dict | None: async def _resolve_subagents( config: AgentConfig, mcp_tool_loader: McpToolLoader | None = None, + prompt_manager: PromptManager | None = None, ) -> list | None: """Convertit les configs de sous-agents. @@ -143,6 +144,14 @@ async def _resolve_subagents( all_tools = (local_tools or []) + mcp_tools if (local_tools or mcp_tools) else None instructions = sa.instructions + if prompt_manager: + try: + content = await prompt_manager.get_prompt_content(sa.name) + instructions = content.get("content") + except Exception: + logger.warning(f"Could not load system prompt for sub-agent '{sa.name}' from Phoenix, using YAML instructions if available.") + instructions = sa.instructions + if sa.response_format: response_tool = _create_response_tool(sa.response_format) all_tools = (all_tools or []) + [response_tool] @@ -232,7 +241,7 @@ async def create_agent_from_config( if config.response_format: kwargs["response_format"] = ProviderStrategy(config.response_format) - subagents = await _resolve_subagents(config, mcp_tool_loader) + subagents = await _resolve_subagents(config, mcp_tool_loader, prompt_manager) if subagents: kwargs["subagents"] = subagents logger.info("Agent '%s' has %d subagents", config.name, len(subagents)) From b28f8f496ea96efd7fd0d65e8177e7621d82954c Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Tue, 7 Apr 2026 21:54:57 +0100 Subject: [PATCH 08/11] BRIC-2: clean code --- pyproject.toml | 1 + src/application/use_cases/create_prompt.py | 1 - src/dependencies.py | 4 +- src/domain/entities/prompt.py | 1 + .../phoenix_prompt_adapter.py} | 78 ++++++++++++------- tests/unit/test_phoenix_prompt_manager.py | 10 +-- uv.lock | 11 +++ 7 files changed, 68 insertions(+), 38 deletions(-) rename src/infrastructure/{tracing/phoenix_prompt_manager.py => prompt_management/phoenix_prompt_adapter.py} (69%) diff --git a/pyproject.toml b/pyproject.toml index 096708b..657e528 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "miniopy-async>=1.21.0", "sqlalchemy[asyncio]>=2.0.0", "alembic>=1.13.0", + "cachetools>=7.0.5", ] [project.optional-dependencies] diff --git a/src/application/use_cases/create_prompt.py b/src/application/use_cases/create_prompt.py index 76bd377..0abb324 100644 --- a/src/application/use_cases/create_prompt.py +++ b/src/application/use_cases/create_prompt.py @@ -22,7 +22,6 @@ async def execute( ) -> PromptVersion: """Create a new prompt.""" logger.info(f"Creating prompt: {identifier}") - print(identifier, content, model_name, description, tags) prompt = await self._prompt_manager.create_prompt( identifier=identifier, content=content, diff --git a/src/dependencies.py b/src/dependencies.py index 03f2ec2..ed816ca 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -31,7 +31,7 @@ from src.infrastructure.postgres_repository.adapter import PostgresAgentConfigRepository from src.infrastructure.postgres_thread.adapter import PostgresThreadRepository from src.infrastructure.tracing.noop_adapter import NoopTracingProvider -from src.infrastructure.tracing.phoenix_prompt_manager import PhoenixPromptManagerImpl +from src.infrastructure.prompt_management.phoenix_prompt_adapter import PhoenixPromptManagerProvider from src.infrastructure.yaml_config.adapter import YamlAgentConfigLoader logger = logging.getLogger("composable-agents") @@ -81,7 +81,7 @@ def _create_tracing_provider(settings: Settings): def get_prompt_manager() -> PromptManager: """Provide PromptManager implementation.""" tracing = settings.tracing - return PhoenixPromptManagerImpl( + return PhoenixPromptManagerProvider( base_url=tracing.phoenix_collector_endpoint, api_key=tracing.phoenix_api_key, ) diff --git a/src/domain/entities/prompt.py b/src/domain/entities/prompt.py index bc1a844..7d8da32 100644 --- a/src/domain/entities/prompt.py +++ b/src/domain/entities/prompt.py @@ -10,6 +10,7 @@ class PromptVersion(BaseModel): content: list[dict[str, str]] = Field(..., description="Messages with role/content") model_name: str tags: list[str] = Field(default_factory=list) + created_at: datetime | None = None # optional, Phoenix doesn't always return this class Prompt(BaseModel): diff --git a/src/infrastructure/tracing/phoenix_prompt_manager.py b/src/infrastructure/prompt_management/phoenix_prompt_adapter.py similarity index 69% rename from src/infrastructure/tracing/phoenix_prompt_manager.py rename to src/infrastructure/prompt_management/phoenix_prompt_adapter.py index 14434fe..3525cf0 100644 --- a/src/infrastructure/tracing/phoenix_prompt_manager.py +++ b/src/infrastructure/prompt_management/phoenix_prompt_adapter.py @@ -1,6 +1,7 @@ import logging import os +from cachetools import TTLCache, cached from phoenix.client import Client from phoenix.client.resources.prompts import PromptVersion as PhoenixPromptVersion @@ -10,7 +11,7 @@ logger = logging.getLogger("composable-agents") -class PhoenixPromptManagerImpl(PromptManager): +class PhoenixPromptManagerProvider(PromptManager): """Phoenix implementation of PromptManager port.""" def __init__(self, base_url: str | None = None, api_key: str | None = None): @@ -21,7 +22,7 @@ def __init__(self, base_url: str | None = None, api_key: str | None = None): base_url=base_url, api_key=api_key, ) - logger.info(f"PhoenixPromptManagerImpl initialized with base_url={base_url}") + logger.info(f"PhoenixPromptManagerProvider initialized with base_url={base_url}") except Exception as e: logger.error(f"Failed to initialize Phoenix client: {e}") self._client = None @@ -49,22 +50,27 @@ async def get_prompt( logger.error(f"Error getting prompt {identifier}: {e}") raise - async def get_prompt_content(self, identifier: str, version_id: str | None = None, tag: str | None = None) -> dict[str, str]: - """Get the content of a prompt.""" + @cached(cache=TTLCache(maxsize=10, ttl=300)) + async def get_prompt_content( + self, + identifier: str, + version_id: str | None = None, + tag: str | None = None, + ) -> dict[str, str]: if not self._client: raise RuntimeError("Phoenix client not initialized") - try: - prompt_obj: PhoenixPromptVersion = self._client.prompts.get( + prompt_obj = self._client.prompts.get( prompt_identifier=identifier, prompt_version_id=version_id, tag=tag, ) - if not prompt_obj: - raise ValueError(f"Prompt not found: {identifier}") - return prompt_obj._template.get("messages", [])[0] if isinstance(prompt_obj._template, dict) else {} + domain = self._to_domain_prompt(prompt_obj, identifier=identifier) + # Return first message (system prompt) or empty + messages = domain.current_version.content + return messages[0] if messages else {} except Exception as e: - logger.error(f"Error getting prompt {identifier}: {e}") + logger.error(f"Error getting prompt content {identifier}: {e}") raise async def create_prompt( @@ -136,29 +142,41 @@ async def add_tag(self, identifier: str, tag: str) -> None: logger.error(f"Error adding tag: {e}") raise - def _to_domain_prompt(self, phoenix_prompt, identifier: str | None = None, description: str | None = None) -> Prompt: + def _to_domain_prompt( + self, + phoenix_prompt, + identifier: str | None = None, + description: str | None = None, + ) -> Prompt: """Convert Phoenix PromptVersion to domain entity.""" - # Extract messages from the internal template - template = getattr(phoenix_prompt, "_template", {}) - messages = template.get("messages", []) if isinstance(template, dict) else [] - - # Extract model name - model_name = getattr(phoenix_prompt, "_model_name", "") - # Extract description - desc = description or getattr(phoenix_prompt, "_description", None) - - # Extract version id - version_id = phoenix_prompt.id or "v1" - - domain_version = PromptVersion( - version_id=version_id, - content=messages, - model_name=model_name, - ) + template = getattr(phoenix_prompt, "_template", {}) + raw_messages = template.get("messages", []) if isinstance(template, dict) else [] + + # Normalize Phoenix message format → domain format + messages = [] + for msg in raw_messages: + role = msg.get("role", "") + raw_content = msg.get("content", "") + # Phoenix stores content as list of blocks or plain string + if isinstance(raw_content, list): + text = " ".join( + block.get("text", "") for block in raw_content + if isinstance(block, dict) and block.get("type") == "text" + ) + else: + text = str(raw_content) + messages.append({"role": role, "content": text}) return Prompt( identifier=identifier or "", - description=desc, - current_version=domain_version + description=description or getattr(phoenix_prompt, "_description", None), + current_version=PromptVersion( + version_id=phoenix_prompt.id or "v1", + content=messages, + model_name=getattr(phoenix_prompt, "_model_name", ""), + tags=[], + ), + created_at=None, + updated_at=None, ) diff --git a/tests/unit/test_phoenix_prompt_manager.py b/tests/unit/test_phoenix_prompt_manager.py index 6b3e59c..ae19d42 100644 --- a/tests/unit/test_phoenix_prompt_manager.py +++ b/tests/unit/test_phoenix_prompt_manager.py @@ -1,4 +1,4 @@ -"""Tests for PhoenixPromptManagerImpl.""" +"""Tests for PhoenixPromptManagerProvider.""" from datetime import datetime from unittest.mock import MagicMock, patch @@ -6,14 +6,14 @@ import pytest from phoenix.client.resources.prompts import PromptVersion as PhoenixPromptVersion -from src.infrastructure.tracing.phoenix_prompt_manager import PhoenixPromptManagerImpl +from src.infrastructure.prompt_management.phoenix_prompt_adapter import PhoenixPromptManagerProvider -class TestPhoenixPromptManagerImpl: +class TestPhoenixPromptManagerProvider: @pytest.fixture def manager(self): - with patch("src.infrastructure.tracing.phoenix_prompt_manager.Client"): - return PhoenixPromptManagerImpl(base_url="http://localhost:6006", api_key="test-key") + with patch("src.infrastructure.prompt_management.phoenix_prompt_adapter.Client"): + return PhoenixPromptManagerProvider(base_url="http://localhost:6006", api_key="test-key") @pytest.mark.asyncio async def test_create_prompt_success(self, manager): diff --git a/uv.lock b/uv.lock index ea2be88..3fd7aab 100644 --- a/uv.lock +++ b/uv.lock @@ -362,6 +362,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, ] +[[package]] +name = "cachetools" +version = "7.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -542,6 +551,7 @@ source = { editable = "." } dependencies = [ { name = "alembic" }, { name = "asyncpg" }, + { name = "cachetools" }, { name = "cryptography" }, { name = "deepagents" }, { name = "fastapi" }, @@ -595,6 +605,7 @@ requires-dist = [ { name = "arize-phoenix-otel", marker = "extra == 'phoenix'", specifier = ">=0.1.0" }, { name = "arize-phoenix-otel", marker = "extra == 'tracing'", specifier = ">=0.1.0" }, { name = "asyncpg", specifier = ">=0.30.0" }, + { name = "cachetools", specifier = ">=7.0.5" }, { name = "cryptography", specifier = ">=46.0.5" }, { name = "deepagents", specifier = ">=0.3.12" }, { name = "fastapi", specifier = ">=0.128.4" }, From 3ce99019e15e6e88e3b4aa0991b56e7f34a702b2 Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Tue, 7 Apr 2026 22:56:29 +0100 Subject: [PATCH 09/11] BRIC-2: add prompt tagging and metadata for agent type and project name --- README.md | 7 +++++-- src/application/requests/prompt.py | 2 ++ src/application/routes/prompt.py | 2 ++ src/application/use_cases/create_prompt.py | 2 ++ src/application/use_cases/update_prompt.py | 2 ++ src/domain/ports/prompt_manager.py | 2 ++ .../prompt_management/phoenix_prompt_adapter.py | 9 +++++++-- tests/unit/test_phoenix_prompt_manager.py | 4 ++-- 8 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 43bf8b7..1b51d0a 100644 --- a/README.md +++ b/README.md @@ -588,7 +588,8 @@ curl -X POST http://localhost:8000/prompts/create \ ], "model_name": "claude-sonnet-4-5-20250929", "description": "Prompt for general customer support queries", - "tags": ["support", "production"] + "tags": ["production"], + "metadata": {"project_name": "composable-agents", "agent_type": "deep_agent"} }' ``` @@ -663,7 +664,9 @@ curl -X PUT http://localhost:8000/prompts/update/customer-support \ } ], "model_name": "claude-sonnet-4-5-20250929", - "description": "Updated prompt for customer support (more detailed)" + "description": "Updated prompt for customer support (more detailed)", + "tags": ["production"], + "metadata": {"project_name": "composable-agents", "agent_type": "deep_agent"} }' ``` diff --git a/src/application/requests/prompt.py b/src/application/requests/prompt.py index 74da0ae..729473c 100644 --- a/src/application/requests/prompt.py +++ b/src/application/requests/prompt.py @@ -7,9 +7,11 @@ class CreatePromptRequest(BaseModel): model_name: str description: str | None = None tags: list[str] | None = None + metadata : dict | None = None class UpdatePromptRequest(BaseModel): content: list[dict[str, str]] | None = None model_name: str | None = None description: str | None = None + metadata : dict | None = None diff --git a/src/application/routes/prompt.py b/src/application/routes/prompt.py index 669eb87..43cddeb 100644 --- a/src/application/routes/prompt.py +++ b/src/application/routes/prompt.py @@ -31,6 +31,7 @@ async def create_prompt( model_name=request.model_name, description=request.description, tags=request.tags, + metadata=request.metadata, ) return {"status": "success", "prompt": prompt} except Exception as e: @@ -73,6 +74,7 @@ async def update_prompt( content=request.content, model_name=request.model_name, description=request.description, + metadata=request.metadata, ) return {"status": "success", "prompt": prompt} except Exception as e: diff --git a/src/application/use_cases/create_prompt.py b/src/application/use_cases/create_prompt.py index 0abb324..de57d7d 100644 --- a/src/application/use_cases/create_prompt.py +++ b/src/application/use_cases/create_prompt.py @@ -19,6 +19,7 @@ async def execute( model_name: str, description: str | None = None, tags: list[str] | None = None, + metadata: dict | None = None, ) -> PromptVersion: """Create a new prompt.""" logger.info(f"Creating prompt: {identifier}") @@ -28,6 +29,7 @@ async def execute( model_name=model_name, description=description, tags=tags, + metadata=metadata, ) logger.info(f"Prompt created successfully: {identifier}") return prompt diff --git a/src/application/use_cases/update_prompt.py b/src/application/use_cases/update_prompt.py index 2b20aef..76b5d81 100644 --- a/src/application/use_cases/update_prompt.py +++ b/src/application/use_cases/update_prompt.py @@ -19,6 +19,7 @@ async def execute( content: list[dict[str, str]] | None = None, model_name: str | None = None, description: str | None = None, + metadata: dict | None = None, ) -> PromptVersion: """Update a prompt.""" logger.info(f"Updating prompt: {identifier}") @@ -27,6 +28,7 @@ async def execute( content=content, model_name=model_name, description=description, + metadata=metadata, ) logger.info(f"Prompt updated successfully: {identifier}") return prompt diff --git a/src/domain/ports/prompt_manager.py b/src/domain/ports/prompt_manager.py index 504e38c..f382420 100644 --- a/src/domain/ports/prompt_manager.py +++ b/src/domain/ports/prompt_manager.py @@ -31,6 +31,7 @@ async def create_prompt( model_name: str, description: str | None = None, tags: list[str] | None = None, + metadata: dict | None = None, ) -> PromptVersion: """Create a new prompt.""" ... @@ -42,6 +43,7 @@ async def update_prompt( content: list[dict[str, str]] | None = None, model_name: str | None = None, description: str | None = None, + metadata: dict | None = None, ) -> PromptVersion: """Update an existing prompt (creates new version).""" ... diff --git a/src/infrastructure/prompt_management/phoenix_prompt_adapter.py b/src/infrastructure/prompt_management/phoenix_prompt_adapter.py index 3525cf0..3dc5c7c 100644 --- a/src/infrastructure/prompt_management/phoenix_prompt_adapter.py +++ b/src/infrastructure/prompt_management/phoenix_prompt_adapter.py @@ -80,6 +80,7 @@ async def create_prompt( model_name: str, description: str | None = None, tags: list[str] | None = None, + metadata: dict | None = None, ) -> PhoenixPromptVersion: """Create a new prompt in Phoenix.""" if not self._client: @@ -90,12 +91,16 @@ async def create_prompt( name=identifier, version=PhoenixPromptVersion(content, model_name=model_name), prompt_description=description, + prompt_metadata=metadata, ) - if tags: + if tags and prompt_obj.id: for tag in tags: try: - self._client.prompts.tag(prompt_identifier=identifier, tag=tag) + self._client.prompts.tags.create( + prompt_version_id=prompt_obj.id, + name=tag, + ) except Exception as tag_error: logger.warning(f"Failed to add tag {tag}: {tag_error}") diff --git a/tests/unit/test_phoenix_prompt_manager.py b/tests/unit/test_phoenix_prompt_manager.py index ae19d42..bf4e66b 100644 --- a/tests/unit/test_phoenix_prompt_manager.py +++ b/tests/unit/test_phoenix_prompt_manager.py @@ -52,7 +52,7 @@ async def test_create_prompt_with_tags(self, manager): mock_prompt_obj.tags = [] manager._client.prompts.create = MagicMock(return_value=mock_prompt_obj) - manager._client.prompts.tag = MagicMock() + manager._client.prompts.tag.create = MagicMock() await manager.create_prompt( identifier="test-prompt", @@ -61,7 +61,7 @@ async def test_create_prompt_with_tags(self, manager): tags=["tag1", "tag2"], ) - assert manager._client.prompts.tag.call_count == 2 + assert manager._client.prompts.tags.create.call_count == 2 @pytest.mark.asyncio async def test_get_prompt_not_found(self, manager): From d46cb1e3e141538d82d8519e95454750368d39ef Mon Sep 17 00:00:00 2001 From: Guemri-Jawher Date: Tue, 7 Apr 2026 23:27:25 +0100 Subject: [PATCH 10/11] BRIC-2: fix missing model in agent reviewer --- agents/code-reviewer.yaml | 2 ++ src/application/routes/prompt.py | 2 +- src/infrastructure/deepagent/factory.py | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/agents/code-reviewer.yaml b/agents/code-reviewer.yaml index 009d2e4..dc53b8a 100644 --- a/agents/code-reviewer.yaml +++ b/agents/code-reviewer.yaml @@ -17,8 +17,10 @@ hitl: - reject subagents: - name: security-auditor + model: "openai:anthropic/claude-haiku-4.5:nitro" description: "Specialized in security vulnerability analysis" instructions: "Focus on OWASP Top 10 and common security patterns" - name: performance-analyst + model: "openai:anthropic/claude-haiku-4.5:nitro" description: "Specialized in performance optimization" instructions: "Analyze time complexity, memory usage, and bottlenecks" diff --git a/src/application/routes/prompt.py b/src/application/routes/prompt.py index 43cddeb..8e6cde0 100644 --- a/src/application/routes/prompt.py +++ b/src/application/routes/prompt.py @@ -38,6 +38,7 @@ async def create_prompt( logger.error(f"Error creating prompt: {e}") raise HTTPException(status_code=500, detail=str(e)) + @router.get("/get/{identifier}") async def get_prompt( identifier: str, @@ -59,7 +60,6 @@ async def get_prompt( raise HTTPException(status_code=404, detail=str(e)) - @router.put("/update/{identifier}") async def update_prompt( identifier: str, diff --git a/src/infrastructure/deepagent/factory.py b/src/infrastructure/deepagent/factory.py index fa115d1..570d2e2 100644 --- a/src/infrastructure/deepagent/factory.py +++ b/src/infrastructure/deepagent/factory.py @@ -245,8 +245,11 @@ async def create_agent_from_config( if subagents: kwargs["subagents"] = subagents logger.info("Agent '%s' has %d subagents", config.name, len(subagents)) - - graph = create_deep_agent(**kwargs) + try: + graph = create_deep_agent(**kwargs) + except Exception as e: + logger.error(f"Error creating agent '{config.name}': {e}") + raise logger.info("Agent '%s' created successfully", config.name) return graph From a4c356dce747f1eaed2322b14a20fdaed9bb4511 Mon Sep 17 00:00:00 2001 From: Kaiohz Date: Thu, 23 Apr 2026 19:31:57 +0200 Subject: [PATCH 11/11] feat: add CD workflow and update dependencies --- .github/workflows/cd.yaml | 126 ++++++ pyproject.toml | 1 + uv.lock | 827 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 918 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/cd.yaml diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..f9edc64 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,126 @@ +name: CD +on: + pull_request: + types: [closed] + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + +jobs: + build-and-push: + if: > + (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || + (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/deploy')) + runs-on: ubuntu-latest + steps: + - name: Get deploy SHA + id: sha + uses: actions/github-script@v7 + with: + script: | + if (context.eventName === 'issue_comment') { + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + return pr.data.head.sha; + } + return context.sha; + result-encoding: string + + - name: Add deploy reaction + if: github.event_name == 'issue_comment' + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket', + }); + + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ steps.sha.outputs.result }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build Docker image for scanning (amd64) + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + load: true + platforms: linux/amd64 + tags: kaiohz/pickpro:composable-agents-scan + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Trivy Image Scan (report) + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 + with: + image-ref: 'kaiohz/pickpro:composable-agents-scan' + format: 'table' + severity: 'CRITICAL,HIGH,MEDIUM' + exit-code: '0' + trivy-config: trivy.yaml + trivy-version: 'v0.69.3' + + - name: Trivy Image Scan (CRITICAL gate) + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 + with: + image-ref: 'kaiohz/pickpro:composable-agents-scan' + format: 'table' + severity: 'CRITICAL' + exit-code: '1' + trivy-config: trivy.yaml + trivy-version: 'v0.69.3' + + - name: Build and push Docker image (multi-platform) + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + platforms: linux/amd64,linux/arm64 + tags: | + kaiohz/pickpro:composable-agents-${{ steps.sha.outputs.result }} + kaiohz/pickpro:composable-agents-latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Checkout flux repository + run: | + git clone https://x-access-token:${{ secrets.FLUX_REPO_TOKEN }}@github.com/SoluDevTech/flux.git flux-repo + + - name: Update deployment image tag + run: | + DEPLOYMENT_FILE="flux-repo/dev/composables/composable-agents/deployment.yaml" + if [ -f "$DEPLOYMENT_FILE" ]; then + sed -i 's|image: kaiohz/pickpro:composable-agents-.*|image: kaiohz/pickpro:composable-agents-${{ steps.sha.outputs.result }}|g' "$DEPLOYMENT_FILE" + else + echo "Error: Deployment file not found at $DEPLOYMENT_FILE" + exit 1 + fi + + - name: Commit and push changes + run: | + cd flux-repo + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add dev/composables/composable-agents/deployment.yaml + git commit -m "Update composable-agents image to ${{ steps.sha.outputs.result }}" || echo "No changes to commit" + git push https://x-access-token:${{ secrets.FLUX_REPO_TOKEN }}@github.com/SoluDevTech/flux.git main \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 57db886..ec71003 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ description = "Composable AI agents with YAML configuration, MCP tool integratio requires-python = ">=3.11" dependencies = [ "deepagents>=0.3.12", + "deepagents-cli==0.0.37", "cryptography>=46.0.5", "langchain-core>=1.2.22", "pyasn1>=0.6.3", diff --git a/uv.lock b/uv.lock index aa1ed17..d2fdaaf 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,18 @@ resolution-markers = [ "python_full_version < '3.13'", ] +[[package]] +name = "agent-client-protocol" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/13/3b893421369767e7043cc115d6ef0df417c298b84563be3a12df0416158d/agent_client_protocol-0.9.0.tar.gz", hash = "sha256:f744c48ab9af0f0b4452e5ab5498d61bcab97c26dbe7d6feec5fd36de49be30b", size = 71853, upload-time = "2026-03-26T01:21:00.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/ed/c284543c08aa443a4ef2c8bd120be51da8433dd174c01749b5d87c333f22/agent_client_protocol-0.9.0-py3-none-any.whl", hash = "sha256:06911500b51d8cb69112544e2be01fc5e7db39ef88fecbc3848c5c6f194798ee", size = 56850, upload-time = "2026-03-26T01:20:59.252Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -143,6 +155,15 @@ 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 = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + [[package]] name = "alembic" version = "1.18.4" @@ -177,7 +198,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.78.0" +version = "0.95.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -189,9 +210,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/51/32849a48f9b1cfe80a508fd269b20bd8f0b1357c70ba092890fde5a6a10b/anthropic-0.78.0.tar.gz", hash = "sha256:55fd978ab9b049c61857463f4c4e9e092b24f892519c6d8078cee1713d8af06e", size = 509136, upload-time = "2026-02-05T17:52:04.986Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/cb/b1896da12f12680c39c90af1b9c9fdf75354899317e2a7900ab37fe3a640/anthropic-0.95.0.tar.gz", hash = "sha256:e4d815351489e5627f39806f12561c52b574e69be10d12fcab723264f955c11d", size = 654528, upload-time = "2026-04-14T19:04:34.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/03/2f50931a942e5e13f80e24d83406714672c57964be593fc046d81369335b/anthropic-0.78.0-py3-none-any.whl", hash = "sha256:2a9887d2e99d1b0f9fe08857a1e9fe5d2d4030455dbf9ac65aab052e2efaeac4", size = 405485, upload-time = "2026-02-05T17:52:03.674Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/a0285521eeaacf9ff5d0fad2d437389aefa0adf3db79b0e0bda49f809ce9/anthropic-0.95.0-py3-none-any.whl", hash = "sha256:9fd3503cb666446e28ab5a5d0ec7feda39968399e494bd2cca2f0b927f8aa7a6", size = 627749, upload-time = "2026-04-14T19:04:35.549Z" }, ] [[package]] @@ -344,6 +365,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "blockbuster" +version = "1.5.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "forbiddenfruit", marker = "implementation_name == 'cpython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e0/dcbab602790a576b0b94108c07e2c048e5897df7cc83722a89582d733987/blockbuster-1.5.26.tar.gz", hash = "sha256:cc3ce8c70fa852a97ee3411155f31e4ad2665cd1c6c7d2f8bb1851dab61dc629", size = 36085, upload-time = "2025-12-05T10:43:47.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/c1/84fc6811122f54b20de2e5afb312ee07a3a47a328755587d1e505475239b/blockbuster-1.5.26-py3-none-any.whl", hash = "sha256:f8e53fb2dd4b6c6ec2f04907ddbd063ca7cd1ef587d24448ef4e50e81e3a79bb", size = 13226, upload-time = "2025-12-05T10:43:48.778Z" }, +] + [[package]] name = "bracex" version = "2.6" @@ -526,6 +572,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -547,6 +602,7 @@ dependencies = [ { name = "cachetools" }, { name = "cryptography" }, { name = "deepagents" }, + { name = "deepagents-cli" }, { name = "fastapi" }, { name = "langchain-core" }, { name = "langchain-mcp-adapters" }, @@ -584,6 +640,7 @@ requires-dist = [ { name = "cachetools", specifier = ">=7.0.5" }, { name = "cryptography", specifier = ">=46.0.5" }, { name = "deepagents", specifier = ">=0.3.12" }, + { name = "deepagents-cli", specifier = "==0.0.37" }, { name = "fastapi", specifier = ">=0.128.4" }, { name = "langchain-core", specifier = ">=1.2.22" }, { name = "langchain-mcp-adapters", specifier = ">=0.1.0" }, @@ -716,6 +773,18 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "croniter" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/de/5832661ed55107b8a09af3f0a2e71e0957226a59eb1dcf0a445cce6daf20/croniter-6.2.2.tar.gz", hash = "sha256:ba60832a5ec8e12e51b8691c3309a113d1cf6526bdf1a48150ce8ec7a532d0ab", size = 113762, upload-time = "2026-03-15T08:43:48.112Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/39/783980e78cb92c2d7bdb1fc7dbc86e94ccc6d58224d76a7f1f51b6c51e30/croniter-6.2.2-py3-none-any.whl", hash = "sha256:a5d17b1060974d36251ea4faf388233eca8acf0d09cbd92d35f4c4ac8f279960", size = 45422, upload-time = "2026-03-15T08:43:46.626Z" }, +] + [[package]] name = "cryptography" version = "46.0.6" @@ -777,18 +846,73 @@ wheels = [ [[package]] name = "deepagents" -version = "0.3.12" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain" }, { name = "langchain-anthropic" }, { name = "langchain-core" }, { name = "langchain-google-genai" }, + { name = "langsmith" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/9d3512327d48e619567797dffb34c356b2e0c7b0aa505fd3aaef342903d3/deepagents-0.3.12.tar.gz", hash = "sha256:ab2d7e7c47040d364a20cc19cc775294c1e942456652d6c12e0f21011068633c", size = 77962, upload-time = "2026-02-06T21:20:43.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/33/6d4306ebd47a3adce30b49807e3a338f72eeb2d6f44f28ad67458cd9736a/deepagents-0.5.2.tar.gz", hash = "sha256:7e942017b5d6a923deb111bc0ff097d07c0b20b64fd96818561d00a8259525d0", size = 121705, upload-time = "2026-04-10T20:39:48.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3a/fb70ab92f86c952f5666ea5f423432888f9e83bb37c2338e6782a308d98c/deepagents-0.5.2-py3-none-any.whl", hash = "sha256:48da8e85bac79c2d4ddf859c6a3aea75d0e642ac55f9efe07492338c8b8d3d94", size = 137658, upload-time = "2026-04-10T20:39:47.443Z" }, +] + +[[package]] +name = "deepagents-acp" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-client-protocol" }, + { name = "deepagents" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/5e/5690a3cabf3fed5c9e4251c38c0a9e8ed32dfb426fbcdf5d31194aa2ca38/deepagents_acp-0.0.5.tar.gz", hash = "sha256:74e3dedada0961798175c668322fc6fedce2a0d8828ffe221df8b26e68d7292a", size = 13005074, upload-time = "2026-04-07T17:31:10.61Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/0a/2b8542a19bb22cf49827a38d04e045c6dba97d03dddc721d3ed2e7be0e5e/deepagents-0.3.12-py3-none-any.whl", hash = "sha256:42e707a1be48db3bc445fbe3243b6dc19333565cac4eab8cdc0e37d780c6cfe7", size = 88553, upload-time = "2026-02-06T21:20:42.575Z" }, + { url = "https://files.pythonhosted.org/packages/51/64/11da85aaeda8778e4d5a0717c4d20acb5f3bfb86a05386a33a19c8da75ce/deepagents_acp-0.0.5-py3-none-any.whl", hash = "sha256:4d4a8a2786e50c4307fd9138d876336c75a296f76df21fe9ea51e4a50760b9c4", size = 16985, upload-time = "2026-04-07T17:31:09.565Z" }, +] + +[[package]] +name = "deepagents-cli" +version = "0.0.37" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiosqlite" }, + { name = "deepagents" }, + { name = "deepagents-acp" }, + { name = "httpx" }, + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langchain-google-genai" }, + { name = "langchain-mcp-adapters" }, + { name = "langchain-openai" }, + { name = "langgraph" }, + { name = "langgraph-checkpoint-sqlite" }, + { name = "langgraph-cli", extra = ["inmem"] }, + { name = "langgraph-sdk" }, + { name = "langsmith", extra = ["sandbox"] }, + { name = "markdownify" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "prompt-toolkit" }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, + { name = "tavily-python" }, + { name = "textual" }, + { name = "textual-autocomplete" }, + { name = "textual-speedups" }, + { name = "tomli-w" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/09/e764fa1371492278499ca36b00efd1dadac95e7c2395c86c52cf2d30656f/deepagents_cli-0.0.37.tar.gz", hash = "sha256:c2296a403de056a77d121952abde116a5a1eafe72bef5fba0669f0f66bac2baa", size = 1148386, upload-time = "2026-04-10T20:46:55.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/43/63da468b5ff0a7eab655908c85ac9ec50e7dff6d79c260e171159229723e/deepagents_cli-0.0.37-py3-none-any.whl", hash = "sha256:d66ca0d260f70972cdadcca1162317a16289700e285dea700b56650602d415f8", size = 423810, upload-time = "2026-04-10T20:46:53.782Z" }, ] [[package]] @@ -834,6 +958,12 @@ 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 = "forbiddenfruit" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/79/d4f20e91327c98096d605646bdc6a5ffedae820f38d378d3515c42ec5e60/forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253", size = 43756, upload-time = "2021-01-16T21:03:35.401Z" } + [[package]] name = "frozenlist" version = "1.8.0" @@ -1094,6 +1224,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, ] +[[package]] +name = "grpcio-health-checking" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/ac/8eb871f4e47b11abfe45497e6187a582ec680ccd7232706d228474a8c7a5/grpcio_health_checking-1.78.0.tar.gz", hash = "sha256:78526d5c60b9b99fd18954b89f86d70033c702e96ad6ccc9749baf16136979b3", size = 17008, upload-time = "2026-02-06T10:01:47.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/30/dbaf47e2210697e2923b49eb62a6a2c07d5ee55bb40cff1e6cc0c5bb22e1/grpcio_health_checking-1.78.0-py3-none-any.whl", hash = "sha256:309798c098c5de72a9bff7172d788fdf309d246d231db9955b32e7c1c773fbeb", size = 19010, upload-time = "2026-02-06T10:01:37.949Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/d1/cbefe328653f746fd319c4377836a25ba64226e41c6a1d7d5cdbc87a459f/grpcio_tools-1.78.0.tar.gz", hash = "sha256:4b0dd86560274316e155d925158276f8564508193088bc43e20d3f5dff956b2b", size = 5393026, upload-time = "2026-02-06T09:59:59.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/78/280184d19242ed6762bf453c47a70b869b3c5c72a24dc5bf2bf43909faa3/grpcio_tools-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:6a8b8b7b49f319d29dbcf507f62984fa382d1d10437d75c3f26db5f09c4ac0af", size = 2545904, upload-time = "2026-02-06T09:57:52.769Z" }, + { url = "https://files.pythonhosted.org/packages/5b/51/3c46dea5113f68fe879961cae62d34bb7a3c308a774301b45d614952ee98/grpcio_tools-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d62cf3b68372b0c6d722a6165db41b976869811abeabc19c8522182978d8db10", size = 5709078, upload-time = "2026-02-06T09:57:56.389Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2c/dc1ae9ec53182c96d56dfcbf3bcd3e55a8952ad508b188c75bf5fc8993d4/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fa9056742efeaf89d5fe14198af71e5cbc4fbf155d547b89507e19d6025906c6", size = 2591744, upload-time = "2026-02-06T09:57:58.341Z" }, + { url = "https://files.pythonhosted.org/packages/04/63/9b53fc9a9151dd24386785171a4191ee7cb5afb4d983b6a6a87408f41b28/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e3191af125dcb705aa6bc3856ba81ba99b94121c1b6ebee152e66ea084672831", size = 2905113, upload-time = "2026-02-06T09:58:00.38Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/0ad8d789f3a2a00893131c140865605fa91671a6e6fcf9da659e1fabba10/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:283239ddbb67ae83fac111c61b25d8527a1dbd355b377cbc8383b79f1329944d", size = 2656436, upload-time = "2026-02-06T09:58:03.038Z" }, + { url = "https://files.pythonhosted.org/packages/09/4d/580f47ce2fc61b093ade747b378595f51b4f59972dd39949f7444b464a03/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac977508c0db15301ef36d6c79769ec1a6cc4e3bc75735afca7fe7e360cead3a", size = 3106128, upload-time = "2026-02-06T09:58:05.064Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/d83b2d89f8d10e438bad36b1eb29356510fb97e81e6a608b22ae1890e8e6/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4ff605e25652a0bd13aa8a73a09bc48669c68170902f5d2bf1468a57d5e78771", size = 3654953, upload-time = "2026-02-06T09:58:07.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/917ce85633311e54fefd7e6eb1224fb780ef317a4d092766f5630c3fc419/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0197d7b561c79be78ab93d0fe2836c8def470683df594bae3ac89dd8e5c821b2", size = 3322630, upload-time = "2026-02-06T09:58:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/b2/55/3fbf6b26ab46fc79e1e6f7f4e0993cf540263dad639290299fad374a0829/grpcio_tools-1.78.0-cp311-cp311-win32.whl", hash = "sha256:28f71f591f7f39555863ced84fcc209cbf4454e85ef957232f43271ee99af577", size = 993804, upload-time = "2026-02-06T09:58:13.698Z" }, + { url = "https://files.pythonhosted.org/packages/73/86/4affe006d9e1e9e1c6653d6aafe2f8b9188acb2b563cd8ed3a2c7c0e8aec/grpcio_tools-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a6de495dabf86a3b40b9a7492994e1232b077af9d63080811838b781abbe4e8", size = 1158566, upload-time = "2026-02-06T09:58:15.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ae/5b1fa5dd8d560a6925aa52de0de8731d319f121c276e35b9b2af7cc220a2/grpcio_tools-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:9eb122da57d4cad7d339fc75483116f0113af99e8d2c67f3ef9cae7501d806e4", size = 2546823, upload-time = "2026-02-06T09:58:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ed/d33ccf7fa701512efea7e7e23333b748848a123e9d3bbafde4e126784546/grpcio_tools-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d0c501b8249940b886420e6935045c44cb818fa6f265f4c2b97d5cff9cb5e796", size = 5706776, upload-time = "2026-02-06T09:58:20.944Z" }, + { url = "https://files.pythonhosted.org/packages/c6/69/4285583f40b37af28277fc6b867d636e3b10e1b6a7ebd29391a856e1279b/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:77e5aa2d2a7268d55b1b113f958264681ef1994c970f69d48db7d4683d040f57", size = 2593972, upload-time = "2026-02-06T09:58:23.29Z" }, + { url = "https://files.pythonhosted.org/packages/d7/eb/ecc1885bd6b3147f0a1b7dff5565cab72f01c8f8aa458f682a1c77a9fb08/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e3c0b0e6ba5275322ba29a97bf890565a55f129f99a21b121145e9e93a22525", size = 2905531, upload-time = "2026-02-06T09:58:25.406Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a9/511d0040ced66960ca10ba0f082d6b2d2ee6dd61837b1709636fdd8e23b4/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975d4cb48694e20ebd78e1643e5f1cd94cdb6a3d38e677a8e84ae43665aa4790", size = 2656909, upload-time = "2026-02-06T09:58:28.022Z" }, + { url = "https://files.pythonhosted.org/packages/06/a3/3d2c707e7dee8df842c96fbb24feb2747e506e39f4a81b661def7fed107c/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:553ff18c5d52807dedecf25045ae70bad7a3dbba0b27a9a3cdd9bcf0a1b7baec", size = 3109778, upload-time = "2026-02-06T09:58:30.091Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4b/646811ba241bf05da1f0dc6f25764f1c837f78f75b4485a4210c84b79eae/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8c7f5e4af5a84d2e96c862b1a65e958a538237e268d5f8203a3a784340975b51", size = 3658763, upload-time = "2026-02-06T09:58:32.875Z" }, + { url = "https://files.pythonhosted.org/packages/45/de/0a5ef3b3e79d1011375f5580dfee3a9c1ccb96c5f5d1c74c8cee777a2483/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96183e2b44afc3f9a761e9d0f985c3b44e03e8bb98e626241a6cbfb3b6f7e88f", size = 3325116, upload-time = "2026-02-06T09:58:34.894Z" }, + { url = "https://files.pythonhosted.org/packages/95/d2/6391b241ad571bc3e71d63f957c0b1860f0c47932d03c7f300028880f9b8/grpcio_tools-1.78.0-cp312-cp312-win32.whl", hash = "sha256:2250e8424c565a88573f7dc10659a0b92802e68c2a1d57e41872c9b88ccea7a6", size = 993493, upload-time = "2026-02-06T09:58:37.242Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/7d0d3a39ecad76ccc136be28274daa660569b244fa7d7d0bbb24d68e5ece/grpcio_tools-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:217d1fa29de14d9c567d616ead7cb0fef33cde36010edff5a9390b00d52e5094", size = 1158423, upload-time = "2026-02-06T09:58:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/ce/17311fb77530420e2f441e916b347515133e83d21cd6cc77be04ce093d5b/grpcio_tools-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2d6de1cc23bdc1baafc23e201b1e48c617b8c1418b4d8e34cebf72141676e5fb", size = 2546284, upload-time = "2026-02-06T09:58:43.073Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/79e101483115f0e78223397daef71751b75eba7e92a32060c10aae11ca64/grpcio_tools-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2afeaad88040894c76656202ff832cb151bceb05c0e6907e539d129188b1e456", size = 5705653, upload-time = "2026-02-06T09:58:45.533Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a7/52fa3ccb39ceeee6adc010056eadfbca8198651c113e418dafebbdf2b306/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33cc593735c93c03d63efe7a8ba25f3c66f16c52f0651910712490244facad72", size = 2592788, upload-time = "2026-02-06T09:58:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/682ff6bb548225513d73dc9403742d8975439d7469c673bc534b9bbc83a7/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2921d7989c4d83b71f03130ab415fa4d66e6693b8b8a1fcbb7a1c67cff19b812", size = 2905157, upload-time = "2026-02-06T09:58:51.478Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/264f3836a96423b7018e5ada79d62576a6401f6da4e1f4975b18b2be1265/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6a0df438e82c804c7b95e3f311c97c2f876dcc36376488d5b736b7bcf5a9b45", size = 2656166, upload-time = "2026-02-06T09:58:54.117Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/f108276611522e03e98386b668cc7e575eff6952f2db9caa15b2a3b3e883/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9c6070a9500798225191ef25d0055a15d2c01c9c8f2ee7b681fffa99c98c822", size = 3109110, upload-time = "2026-02-06T09:58:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/cf048dbcd64b3396b3c860a2ffbcc67a8f8c87e736aaa74c2e505a7eee4c/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:394e8b57d85370a62e5b0a4d64c96fcf7568345c345d8590c821814d227ecf1d", size = 3657863, upload-time = "2026-02-06T09:58:59.176Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e2736912c8fda57e2e57a66ea5e0bc8eb9a5fb7ded00e866ad22d50afb08/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3ef700293ab375e111a2909d87434ed0a0b086adf0ce67a8d9cf12ea7765e63", size = 3324748, upload-time = "2026-02-06T09:59:01.242Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/726abc75bb5bfc2841e88ea05896e42f51ca7c30cb56da5c5b63058b3867/grpcio_tools-1.78.0-cp313-cp313-win32.whl", hash = "sha256:6993b960fec43a8d840ee5dc20247ef206c1a19587ea49fe5e6cc3d2a09c1585", size = 993074, upload-time = "2026-02-06T09:59:03.085Z" }, + { url = "https://files.pythonhosted.org/packages/c5/68/91b400bb360faf9b177ffb5540ec1c4d06ca923691ddf0f79e2c9683f4da/grpcio_tools-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:275ce3c2978842a8cf9dd88dce954e836e590cf7029649ad5d1145b779039ed5", size = 1158185, upload-time = "2026-02-06T09:59:05.036Z" }, + { url = "https://files.pythonhosted.org/packages/cf/5e/278f3831c8d56bae02e3acc570465648eccf0a6bbedcb1733789ac966803/grpcio_tools-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b080d0d072e6032708a3a91731b808074d7ab02ca8fb9847b6a011fdce64cd9", size = 2546270, upload-time = "2026-02-06T09:59:07.426Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d9/68582f2952b914b60dddc18a2e3f9c6f09af9372b6f6120d6cf3ec7f8b4e/grpcio_tools-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8c0ad8f8f133145cd7008b49cb611a5c6a9d89ab276c28afa17050516e801f79", size = 5705731, upload-time = "2026-02-06T09:59:09.856Z" }, + { url = "https://files.pythonhosted.org/packages/70/68/feb0f9a48818ee1df1e8b644069379a1e6ef5447b9b347c24e96fd258e5d/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2f8ea092a7de74c6359335d36f0674d939a3c7e1a550f4c2c9e80e0226de8fe4", size = 2593896, upload-time = "2026-02-06T09:59:12.23Z" }, + { url = "https://files.pythonhosted.org/packages/1f/08/a430d8d06e1b8d33f3e48d3f0cc28236723af2f35e37bd5c8db05df6c3aa/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:da422985e0cac822b41822f43429c19ecb27c81ffe3126d0b74e77edec452608", size = 2905298, upload-time = "2026-02-06T09:59:14.458Z" }, + { url = "https://files.pythonhosted.org/packages/71/0a/348c36a3eae101ca0c090c9c3bc96f2179adf59ee0c9262d11cdc7bfe7db/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4fab1faa3fbcb246263e68da7a8177d73772283f9db063fb8008517480888d26", size = 2656186, upload-time = "2026-02-06T09:59:16.949Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3f/18219f331536fad4af6207ade04142292faa77b5cb4f4463787988963df8/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dd9c094f73f734becae3f20f27d4944d3cd8fb68db7338ee6c58e62fc5c3d99f", size = 3109859, upload-time = "2026-02-06T09:59:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d9/341ea20a44c8e5a3a18acc820b65014c2e3ea5b4f32a53d14864bcd236bc/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2ed51ce6b833068f6c580b73193fc2ec16468e6bc18354bc2f83a58721195a58", size = 3657915, upload-time = "2026-02-06T09:59:21.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/5978b0f91611a64371424c109dd0027b247e5b39260abad2eaee66b6aa37/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05803a5cdafe77c8bdf36aa660ad7a6a1d9e49bc59ce45c1bade2a4698826599", size = 3324724, upload-time = "2026-02-06T09:59:24.402Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/96a324dba99cfbd20e291baf0b0ae719dbb62b76178c5ce6c788e7331cb1/grpcio_tools-1.78.0-cp314-cp314-win32.whl", hash = "sha256:f7c722e9ce6f11149ac5bddd5056e70aaccfd8168e74e9d34d8b8b588c3f5c7c", size = 1015505, upload-time = "2026-02-06T09:59:26.3Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d1/909e6a05bfd44d46327dc4b8a78beb2bae4fb245ffab2772e350081aaf7e/grpcio_tools-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d58ade518b546120ec8f0a8e006fc8076ae5df151250ebd7e82e9b5e152c229", size = 1190196, upload-time = "2026-02-06T09:59:28.359Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1327,6 +1523,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] +[[package]] +name = "jsonschema-rs" +version = "0.44.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/88/f0cc7013ad6a3d0b86275a6d0a3112eaa705545c89134ab2a057865c054c/jsonschema_rs-0.44.1.tar.gz", hash = "sha256:49ca909cc3017990a732145b9a7c2f1a0727b2f95dba4190c05a514575b5f4bf", size = 1975289, upload-time = "2026-03-03T19:08:21.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/59/57efa11b8a7069687c7d741849a75092cbb4a6bdce30d52a2832a168c3c5/jsonschema_rs-0.44.1-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6f8be6467ee403e126e4e0abb68f13cfbf7199db54d5a4c0f2a1b00e1304f2e3", size = 7365683, upload-time = "2026-03-03T19:07:34.512Z" }, + { url = "https://files.pythonhosted.org/packages/02/39/b1ec92bd383d9e8e0cd70f019f0c047313e4980a3f7e653cfb3270a84310/jsonschema_rs-0.44.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:95434b4858da6feb4b3769c955b78204dbc90988941e9e848596ab93c6005d00", size = 3828559, upload-time = "2026-03-03T19:07:36.965Z" }, + { url = "https://files.pythonhosted.org/packages/39/97/0b581ce2ca6b6ca3f29cea189609c893aa3c033356a7cb6950cb7559bdc0/jsonschema_rs-0.44.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0329af23e7674d88c3117b55c89a0c36e06ee359e696be16796a29c8b1c33e85", size = 3572164, upload-time = "2026-03-03T19:07:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/35/a9/6d750088795947a5366cdfa6b9064680a3b0a86f61806521beb35d88c8fb/jsonschema_rs-0.44.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8078c834c3cea6303796fc4925bb8646d1f68313bd54f6d3dde08c8b8eb74bc1", size = 3926333, upload-time = "2026-03-03T19:07:40.369Z" }, + { url = "https://files.pythonhosted.org/packages/a8/19/6475da01b4e81c0445698290a7b8f237e678a0dc9fbf55df663243597b70/jsonschema_rs-0.44.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:502af60c802cf149185ea01edbd31a143b09aaf06b27b6422f8b8893984b1998", size = 3589764, upload-time = "2026-03-03T19:07:42.113Z" }, + { url = "https://files.pythonhosted.org/packages/fe/43/dd8d1a8dcd3dd44e7242944433d86433540ed71a5906d0d75b5dd4fb3352/jsonschema_rs-0.44.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f2760c4791ecc3c7e6196cec7e7dbf191205e36dd050119cfab421e108e8508", size = 3782136, upload-time = "2026-03-03T19:07:44.505Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/8ada7636eb2119482fecc6289c3115b27cb045384896e45b8bd0fec98d5b/jsonschema_rs-0.44.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:16d663e6c4838e4d594bd9d10c5939a6737c171d9c8600659fe6612098863d3d", size = 4151840, upload-time = "2026-03-03T19:07:46.754Z" }, + { url = "https://files.pythonhosted.org/packages/bf/7e/f163531f203fa4e11871a40a04dc280a94a2b88c2eaa32db7c71cb64b5b4/jsonschema_rs-0.44.1-cp310-abi3-win32.whl", hash = "sha256:cbec5ef1a0cc327cbc829f44a9c76778881003ada99c871a14438c7e8b264e76", size = 3197538, upload-time = "2026-03-03T19:07:48.12Z" }, + { url = "https://files.pythonhosted.org/packages/73/4b/c080db0f50b7a320c80991f3cc9069d865f6a8baadd2952fda7473cf3816/jsonschema_rs-0.44.1-cp310-abi3-win_amd64.whl", hash = "sha256:cee075749f0479599586b4f591940418e45eae65485ed29e84763a28ec9dd40c", size = 3748176, upload-time = "2026-03-03T19:07:49.698Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9f/2f602bf9d3958866f03732abefc51f8bc6caa0f8ea913b8f0ac01923e886/jsonschema_rs-0.44.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:99c0c3e4a786d1e9c25dbd58cc9781f3c3d25c9fbd76310a350de55315f05948", size = 3817433, upload-time = "2026-03-03T19:07:51.161Z" }, + { url = "https://files.pythonhosted.org/packages/42/cf/d899a52ca5fd7846614e15a230845d19070eec865b0791108b14341ef39e/jsonschema_rs-0.44.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:516bfb8926de7d396e4bc9a1c5085870de0035e8e2324014251d091a55a03623", size = 3570909, upload-time = "2026-03-03T19:07:53.117Z" }, + { url = "https://files.pythonhosted.org/packages/53/bb/cc3fda5594cdc3626e479f868f28b5a1d9091296e764ca041d2580d0a292/jsonschema_rs-0.44.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225074845f6a67e8e3ac18311f87a0ab925ae5adf16466be61c7d1df01eca20a", size = 3920084, upload-time = "2026-03-03T19:07:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/bc/75/49e09ce6b72f8d25813842d9184678d6be92f0a3e90f0276a995c5712986/jsonschema_rs-0.44.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:782d01412e77c83bb376d31aac8afbd06b97e3594f09d1e0304ad22c2382077b", size = 3584852, upload-time = "2026-03-03T19:07:56.508Z" }, + { url = "https://files.pythonhosted.org/packages/51/67/4e52d1ab98c8656a66ca1b0422af18da5a5525d6aa23c57be455bdcc6515/jsonschema_rs-0.44.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2afe720dfa1f93235b78e812937039537b63bf4eab6ca3c9ecb7fd7ba08a865d", size = 3776400, upload-time = "2026-03-03T19:07:58.392Z" }, + { url = "https://files.pythonhosted.org/packages/d8/44/5cd424c80df74ad159aceaf59071f26a7b7decf925a952c8929c2e097375/jsonschema_rs-0.44.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:548a1f466ce5b904c9cc52eee8f887c3838377ed95f4525d0ee5896a321e89d5", size = 4144701, upload-time = "2026-03-03T19:08:00.476Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4f/4f8c9a423b2f539b22f0fc314063b724da82df3116a57149c2d730943150/jsonschema_rs-0.44.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8a758e422c4ec265e64f2232409ddc5976b28e94e84a8e5565a2bce169ab72e9", size = 3739509, upload-time = "2026-03-03T19:08:01.97Z" }, + { url = "https://files.pythonhosted.org/packages/76/85/759020a30874df8053c2abf91ed8abe8f27e69e683ed1d94ac2bbf92e7a8/jsonschema_rs-0.44.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ca8ddd724b73678f5f3d3d8f948ae40fa817ad9edd5ce4e732ae26cb0f9dd300", size = 3816826, upload-time = "2026-03-03T19:08:03.411Z" }, + { url = "https://files.pythonhosted.org/packages/45/77/47720717c3008483ff54365f65dbfb264d6dd3b3e7a2367d7f4f0a0a76e4/jsonschema_rs-0.44.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1ff6c9868c8f2834952efa0555fd82d0ab19664ba6b17f481330c64f7af7177d", size = 3569065, upload-time = "2026-03-03T19:08:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/06/88/734eb132228c09b2e0a442da6686efe8bd0c8f0095b13d52b46dcacea735/jsonschema_rs-0.44.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec883313f3782f1c0ffc58ceda55136e26967198523b9cd111af782e273659a3", size = 3918339, upload-time = "2026-03-03T19:08:07.004Z" }, + { url = "https://files.pythonhosted.org/packages/01/9c/6c4ca6c6bc906e4d74425d48c7fd49e558ec4ec98fb792d549c3ed95a632/jsonschema_rs-0.44.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f971acf2910e64f0960080db6b6c73df483318d9db992273885f596cc3a9a5d9", size = 3583541, upload-time = "2026-03-03T19:08:08.683Z" }, + { url = "https://files.pythonhosted.org/packages/14/13/f907c17fc0de4d653cac237846303a164ae58d26656d4161ad4c13d5267a/jsonschema_rs-0.44.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50f5c28fd54236e43f392041f06132b0e9f09dd261cb00236045078d98e3cf84", size = 3774990, upload-time = "2026-03-03T19:08:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/83/1c/141b5b43db5aeac7d54cf3022cfa6a2941fe4d550afacfcf7bbcd49e66fa/jsonschema_rs-0.44.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbc59d68f38a377117b84b8109af269813a39b4b961e803876767e4fab6bac98", size = 4143282, upload-time = "2026-03-03T19:08:12.229Z" }, + { url = "https://files.pythonhosted.org/packages/79/8a/6d4d55583e97d37ca7ac5595d978a83ecfbf8c113ebe31496f7330c72a49/jsonschema_rs-0.44.1-cp314-cp314t-win_amd64.whl", hash = "sha256:049203fd4876f2ec96191c0f8befabf33289988c57e4f191b5fd5974de1fb07f", size = 3738147, upload-time = "2026-03-03T19:08:13.58Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/b623ab4c5727bf6295a143077440ac3598541b226be7cf68c58022598731/jsonschema_rs-0.44.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:51886a0e09161c0f5675ca2834bcd76c086034891c1e0a9a09b2ee2fd7c60bd0", size = 3826241, upload-time = "2026-03-03T19:08:15.013Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/8b5731d9e96cda4a3e46d481921ebfd24809a9451bccc31d7c2a2f80061a/jsonschema_rs-0.44.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46b629a0713397b3375e2926cf3d3f9ad511681d65f7676caee8223f3b62a427", size = 3924348, upload-time = "2026-03-03T19:08:16.454Z" }, + { url = "https://files.pythonhosted.org/packages/6f/44/b12f693f90613402fd60d9a560aaebb65f63de09197582caa8e90532fcca/jsonschema_rs-0.44.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c338c2bf3c5a4e17fccbf504aaf8a00bd1c711f992835df19de2fe55e5cf8b53", size = 3587696, upload-time = "2026-03-03T19:08:18.485Z" }, + { url = "https://files.pythonhosted.org/packages/16/b2/d5e59cf1dca5e72228f05df881e6de0006eb5d536e7bc359015d01357f4d/jsonschema_rs-0.44.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:26c50f9bf4568874a5c6d1ca5c7e739b42529673d2d4c89a2c170800d7983fd4", size = 3746177, upload-time = "2026-03-03T19:08:20.033Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.9.1" @@ -1341,35 +1572,35 @@ wheels = [ [[package]] name = "langchain" -version = "1.2.13" +version = "1.2.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/e5/56fdeedaa0ef1be3c53721d382d9e21c63930179567361610ea6102c04ea/langchain-1.2.13.tar.gz", hash = "sha256:d566ef67c8287e7f2e2df3c99bf3953a6beefd2a75a97fe56ecce905e21f3ef4", size = 573819, upload-time = "2026-03-19T17:16:07.641Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/3f/888a7099d2bd2917f8b0c3ffc7e347f1e664cf64267820b0b923c4f339fc/langchain-1.2.15.tar.gz", hash = "sha256:1717b6719daefae90b2728314a5e2a117ff916291e2862595b6c3d6fba33d652", size = 574732, upload-time = "2026-04-03T14:26:03.994Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/1d/a509af07535d8f4621d77f3ba5ec846ee6d52c59d2239e1385ec3b29bf92/langchain-1.2.13-py3-none-any.whl", hash = "sha256:37d4526ac4b0cdd3d7706a6366124c30dc0771bf5340865b37cdc99d5e5ad9b1", size = 112488, upload-time = "2026-03-19T17:16:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e8/a3b8cb0005553f6a876865073c81ef93bd7c5b18381bcb9ba4013af96ebc/langchain-1.2.15-py3-none-any.whl", hash = "sha256:e349db349cb3e9550c4044077cf90a1717691756cc236438404b23500e615874", size = 112714, upload-time = "2026-04-03T14:26:02.557Z" }, ] [[package]] name = "langchain-anthropic" -version = "1.3.2" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anthropic" }, { name = "langchain-core" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/dd/c5e094079bdd748ca3f0bd0a09189ed2fa46bba56b5a8351198dc7c19e1f/langchain_anthropic-1.3.2.tar.gz", hash = "sha256:e551726a6ebf20229bde06022b5149d33bd48d28e34bd002a744953667b8ad48", size = 686239, upload-time = "2026-02-06T16:14:46.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/c7/259d4d805c6ac90c8695714fc15498a4557bb515eb24f692fd611966e383/langchain_anthropic-1.4.0.tar.gz", hash = "sha256:bbf64e99f9149a34ba67813e9582b2160a0968de9e9f54f7ba8d1658f253c2e5", size = 674360, upload-time = "2026-03-17T18:42:20.751Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/6b/2da16c32308f79bb4588cec7095edbc770722ae4b3c3a1c135e05b0bdc2e/langchain_anthropic-1.3.2-py3-none-any.whl", hash = "sha256:35bc30862696a493680b898eb76bd6c866841f8e48a57d5eca1420a4fd807ac0", size = 46751, upload-time = "2026-02-06T16:14:44.734Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c0/77f99373276d4f06c38a887ef6023f101cfc7ba3b2bf9af37064cdbadde5/langchain_anthropic-1.4.0-py3-none-any.whl", hash = "sha256:c84f55722336935f7574d5771598e674f3959fdca0b51de14c9788dbf52761be", size = 48463, upload-time = "2026-03-17T18:42:19.742Z" }, ] [[package]] name = "langchain-core" -version = "1.2.23" +version = "1.2.29" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1381,14 +1612,14 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/47/a5f21b651e9cbd7a26c3e5809336d10a0be94ef7bdf6bea47f2ad9fff1a8/langchain_core-1.2.23.tar.gz", hash = "sha256:fdec64f90cfea25317e88d9803c44684af1f4e30dec4e58320dd7393bb0f0785", size = 841684, upload-time = "2026-03-27T23:28:14.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/d8/7bdf30e4bfc5175609201806e399506a0a78a48e14367dc8b776a9b4c89c/langchain_core-1.2.29.tar.gz", hash = "sha256:cfb89c92bca81ad083eafcdfe6ec40f9803c9abf7dd166d0f8a8de1d2de03ca6", size = 846121, upload-time = "2026-04-14T20:44:58.117Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/5a/6ff2d76618e4cac531ea51d4ef44c6add36575a84c3f0f8877aee68c951a/langchain_core-1.2.23-py3-none-any.whl", hash = "sha256:70866dfc5275b7840ce272ff70f0ff216c8666ab25dc1b41964a4ef58c02a3ff", size = 506709, upload-time = "2026-03-27T23:28:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/72/37/fed31f80436b1d7bb222f1f2345300a77a88215416acf8d1cb7c8fda7388/langchain_core-1.2.29-py3-none-any.whl", hash = "sha256:11f02e57ee1c24e6e0e6577acbd35df77b205d4692a3df956b03b5389cbe44a0", size = 508733, upload-time = "2026-04-14T20:44:56.712Z" }, ] [[package]] name = "langchain-google-genai" -version = "4.2.0" +version = "4.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filetype" }, @@ -1396,9 +1627,9 @@ dependencies = [ { name = "langchain-core" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/eae2305e207574dc633983a8a82a745e0ede1bce1f3a9daff24d2341fadc/langchain_google_genai-4.2.0.tar.gz", hash = "sha256:9a8d9bfc35354983ed29079cefff53c3e7c9c2a44b6ba75cc8f13a0cf8b55c33", size = 277361, upload-time = "2026-01-13T20:41:17.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/63/e7d148f903cebfef50109da71378f411166f068d66f79b9e16a62dbacf41/langchain_google_genai-4.2.1.tar.gz", hash = "sha256:7f44487a0337535897e3bba9a1d6605d722629e034f757ffa8755af0aa85daa8", size = 278288, upload-time = "2026-02-19T19:29:19.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/51/39942c0083139652494bb354dddf0ed397703a4882302f7b48aeca531c96/langchain_google_genai-4.2.0-py3-none-any.whl", hash = "sha256:856041aaafceff65a4ef0d5acf5731f2db95229ff041132af011aec51e8279d9", size = 66452, upload-time = "2026-01-13T20:41:16.296Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7e/46c5973bd8b10a5c4c8a77136cf536e658796380a17c740246074901b038/langchain_google_genai-4.2.1-py3-none-any.whl", hash = "sha256:a7735289cf94ca3a684d830e09196aac8f6e75e647e3a0a1c3c9dc534ceb985e", size = 66500, upload-time = "2026-02-19T19:29:18.002Z" }, ] [[package]] @@ -1417,21 +1648,21 @@ wheels = [ [[package]] name = "langchain-openai" -version = "1.1.7" +version = "1.1.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/b7/30bfc4d1b658a9ee524bcce3b0b2ec9c45a11c853a13c4f0c9da9882784b/langchain_openai-1.1.7.tar.gz", hash = "sha256:f5ec31961ed24777548b63a5fe313548bc6e0eb9730d6552b8c6418765254c81", size = 1039134, upload-time = "2026-01-07T19:44:59.728Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/63/0fed7cae7103e4b7aced76208aa92c02ae78bdf1be48bd9d83e4051d6c31/langchain_openai-1.1.13.tar.gz", hash = "sha256:88e13342407016785bd3c48be32ded1f28b992403bbb82505b558d81b038adc2", size = 1114743, upload-time = "2026-04-15T01:37:19.409Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/a1/50e7596aca775d8c3883eceeaf47489fac26c57c1abe243c00174f715a8a/langchain_openai-1.1.7-py3-none-any.whl", hash = "sha256:34e9cd686aac1a120d6472804422792bf8080a2103b5d21ee450c9e42d053815", size = 84753, upload-time = "2026-01-07T19:44:58.629Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d1/ca789988897096883289f9597ee653574b67b4b2a8f40bc306dfd73742d5/langchain_openai-1.1.13-py3-none-any.whl", hash = "sha256:54ba1e9f2f0f428aeea68271a87823a0a1b22360283990a713c731d2ef7da926", size = 88723, upload-time = "2026-04-15T01:37:18.062Z" }, ] [[package]] name = "langgraph" -version = "1.1.3" +version = "1.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -1441,9 +1672,50 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/b2/e7db624e8b0ee063ecfbf7acc09467c0836a05914a78e819dfb3744a0fac/langgraph-1.1.3.tar.gz", hash = "sha256:ee496c297a9c93b38d8560be15cbb918110f49077d83abd14976cb13ac3b3370", size = 545120, upload-time = "2026-03-18T23:42:58.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/e5/d3f72ead3c7f15769d5a9c07e373628f1fbaf6cbe7735694d7085859acf6/langgraph-1.1.6.tar.gz", hash = "sha256:1783f764b08a607e9f288dbcf6da61caeb0dd40b337e5c9fb8b412341fbc0b60", size = 549634, upload-time = "2026-04-03T19:01:32.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e6/b36ecdb3ff4ba9a290708d514bae89ebbe2f554b6abbe4642acf3fddbe51/langgraph-1.1.6-py3-none-any.whl", hash = "sha256:fdbf5f54fa5a5a4c4b09b7b5e537f1b2fa283d2f0f610d3457ddeecb479458b9", size = 169755, upload-time = "2026-04-03T19:01:30.686Z" }, +] + +[[package]] +name = "langgraph-api" +version = "0.7.101" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "cryptography" }, + { name = "grpcio" }, + { name = "grpcio-health-checking" }, + { name = "grpcio-tools" }, + { name = "httptools", marker = "sys_platform != 'win32'" }, + { name = "httpx" }, + { name = "jsonschema-rs" }, + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-runtime-inmem" }, + { name = "langgraph-sdk" }, + { name = "langsmith", extra = ["otel"] }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "protobuf" }, + { name = "pyjwt" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "structlog" }, + { name = "tenacity" }, + { name = "truststore" }, + { name = "uuid-utils" }, + { name = "uvicorn" }, + { name = "uvloop", marker = "sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/4e067fe0d5c9165ebe2cdf8c2398b9910d778fe83ba5863087b21dbf95a9/langgraph_api-0.7.101.tar.gz", hash = "sha256:139b56d0d0374c02a1424aaa649c5cad7e3394fe435156c71ce1868391a441d1", size = 661692, upload-time = "2026-04-14T17:41:15.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/f7/221cc479e95e03e260496616e5ce6fb50c1ea01472e3a5bc481a9b8a2f83/langgraph-1.1.3-py3-none-any.whl", hash = "sha256:57cd6964ebab41cbd211f222293a2352404e55f8b2312cecde05e8753739b546", size = 168149, upload-time = "2026-03-18T23:42:56.967Z" }, + { url = "https://files.pythonhosted.org/packages/04/d9/280f6332365d0dd6dd25e2eb858cbf938cbbbd083e96a98273fd6224fc14/langgraph_api-0.7.101-py3-none-any.whl", hash = "sha256:dbee0e3da8d5932abc29e0a17b2c26d5316ece60a6005a0f7e539d55e5c36a8a", size = 546760, upload-time = "2026-04-14T17:41:13.913Z" }, ] [[package]] @@ -1459,35 +1731,89 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/de/ddd53b7032e623f3c7bcdab2b44e8bf635e468f62e10e5ff1946f62c9356/langgraph_checkpoint-4.0.0-py3-none-any.whl", hash = "sha256:3fa9b2635a7c5ac28b338f631abf6a030c3b508b7b9ce17c22611513b589c784", size = 46329, upload-time = "2026-01-12T20:30:25.2Z" }, ] +[[package]] +name = "langgraph-checkpoint-sqlite" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiosqlite" }, + { name = "langgraph-checkpoint" }, + { name = "sqlite-vec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/61/40b7f8f29d6de92406e668c35265f409f57064907e31eae84ab3f2a3e3e1/langgraph_checkpoint_sqlite-3.0.3.tar.gz", hash = "sha256:438c234d37dabda979218954c9c6eb1db73bee6492c2f1d3a00552fe23fa34ed", size = 123876, upload-time = "2026-01-19T00:38:44.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/d8/84ef22ee1cc485c4910df450108fd5e246497379522b3c6cfba896f71bf6/langgraph_checkpoint_sqlite-3.0.3-py3-none-any.whl", hash = "sha256:02eb683a79aa6fcda7cd4de43861062a5d160dbbb990ef8a9fd76c979998a952", size = 33593, upload-time = "2026-01-19T00:38:43.288Z" }, +] + +[[package]] +name = "langgraph-cli" +version = "0.4.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "httpx" }, + { name = "langgraph-sdk" }, + { name = "pathspec" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/42/6320aad6dfe95827e6fbf6a9e835263b554b340c0cf12386cc6a4403471d/langgraph_cli-0.4.21.tar.gz", hash = "sha256:7c03c69d22e0f12267c576cef23b4b29e0a631b83902075c8e75d6c549c9d993", size = 1020652, upload-time = "2026-04-08T01:31:42.883Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/38/6d7231122820bded2467dd88028e2ff2e2210a166f18311ea261ee5998f8/langgraph_cli-0.4.21-py3-none-any.whl", hash = "sha256:34e5a228293339bf45d10a6c713ace742314d6047adcf909acc7d2decbb1b825", size = 73603, upload-time = "2026-04-08T01:31:41.666Z" }, +] + +[package.optional-dependencies] +inmem = [ + { name = "langgraph-api" }, + { name = "langgraph-runtime-inmem" }, +] + [[package]] name = "langgraph-prebuilt" -version = "1.0.8" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/06/dd61a5c2dce009d1b03b1d56f2a85b3127659fdddf5b3be5d8f1d60820fb/langgraph_prebuilt-1.0.8.tar.gz", hash = "sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69", size = 164442, upload-time = "2026-02-19T18:14:39.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/4c/06dac899f4945bedb0c3a1583c19484c2cc894114ea30d9a538dd270086e/langgraph_prebuilt-1.0.9.tar.gz", hash = "sha256:93de7512e9caade4b77ead92428f6215c521fdb71b8ffda8cd55f0ad814e64de", size = 165850, upload-time = "2026-04-03T14:06:37.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/41/ec966424ad3f2ed3996d24079d3342c8cd6c0bd0653c12b2a917a685ec6c/langgraph_prebuilt-1.0.8-py3-none-any.whl", hash = "sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0", size = 35648, upload-time = "2026-02-19T18:14:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/8368ac187b75e7f9d938ca075d34f116683f5cfc48d924029ee79aea147b/langgraph_prebuilt-1.0.9-py3-none-any.whl", hash = "sha256:776c8e3154a5aef5ad0e5bf3f263f2dcaab3983786cc20014b7f955d99d2d1b2", size = 35958, upload-time = "2026-04-03T14:06:36.58Z" }, +] + +[[package]] +name = "langgraph-runtime-inmem" +version = "0.27.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blockbuster" }, + { name = "croniter" }, + { name = "langgraph" }, + { name = "langgraph-checkpoint" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "structlog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/34/3226243f8889da246fea7fa6307148c19a330a0fdc15be9e7e54462f3c88/langgraph_runtime_inmem-0.27.3.tar.gz", hash = "sha256:8f2957ec65c424526be690710a70f65bf906ddd92a4bd2054bb561c8f944ac91", size = 115072, upload-time = "2026-04-08T06:11:29.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/a3/733f3580dd4d1541769139002f847318a3c277386a88153fbb415e3b41d9/langgraph_runtime_inmem-0.27.3-py3-none-any.whl", hash = "sha256:665119e7fa753abb3fe6387cc9dd3d9b8fc79052c2b13f4cc0300efeee349b59", size = 47548, upload-time = "2026-04-08T06:11:28.415Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.3.4" +version = "0.3.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "orjson" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/37/1c18ebb9090a29cd360abce7ee0d3c639fa680e20a078b8c5e85044443d9/langgraph_sdk-0.3.4.tar.gz", hash = "sha256:a8055464027c70ff7b454c0d67caec9a91c6a2bc75c66d023d3ce48773a2a774", size = 132239, upload-time = "2026-02-06T00:44:14.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/db/77a45127dddcfea5e4256ba916182903e4c31dc4cfca305b8c386f0a9e53/langgraph_sdk-0.3.13.tar.gz", hash = "sha256:419ca5663eec3cec192ad194ac0647c0c826866b446073eb40f384f950986cd5", size = 196360, upload-time = "2026-04-07T20:34:18.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/e6/df257026e1370320b60d54492c0847631729ad80ca8d8571b55ece594281/langgraph_sdk-0.3.4-py3-none-any.whl", hash = "sha256:eb73a2fb57a4167aeb31efeaf0c4daecd2cf0c942e8a376670fd1cc636992f49", size = 67833, upload-time = "2026-02-06T00:44:12.795Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/64d64e9f8eea47ce7b939aa6da6863b674c8d418647813c20111645fcc62/langgraph_sdk-0.3.13-py3-none-any.whl", hash = "sha256:aee09e345c90775f6de9d6f4c7b847cfc652e49055c27a2aed0d981af2af3bd0", size = 96668, upload-time = "2026-04-07T20:34:17.866Z" }, ] [[package]] name = "langsmith" -version = "0.6.9" +version = "0.7.31" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -1500,9 +1826,19 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/e0/463a70b43d6755b01598bb59932eec8e2029afcab455b5312c318ac457b5/langsmith-0.6.9.tar.gz", hash = "sha256:aae04cec6e6d8e133f63ba71c332ce0fbd2cda95260db7746ff4c3b6a3c41db1", size = 973557, upload-time = "2026-02-05T20:10:55.629Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/8e/063e09c5e8a3dcd77e2a8f0bff3f71c1c52a9d238da1bcafd2df3281da17/langsmith-0.6.9-py3-none-any.whl", hash = "sha256:86ba521e042397f6fbb79d63991df9d5f7b6a6dd6a6323d4f92131291478dcff", size = 319228, upload-time = "2026-02-05T20:10:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" }, +] + +[package.optional-dependencies] +otel = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, +] +sandbox = [ + { name = "websockets" }, ] [[package]] @@ -1568,6 +1904,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + [[package]] name = "mako" version = "1.3.10" @@ -1580,6 +1928,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markdownify" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09", size = 18816, upload-time = "2025-11-16T19:21:18.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724, upload-time = "2025-11-16T19:21:17.622Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1679,6 +2057,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "miniopy-async" version = "1.23.5" @@ -1862,7 +2261,7 @@ wheels = [ [[package]] name = "openai" -version = "2.17.0" +version = "2.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1874,9 +2273,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/a2/677f22c4b487effb8a09439fb6134034b5f0a39ca27df8b95fac23a93720/openai-2.17.0.tar.gz", hash = "sha256:47224b74bd20f30c6b0a6a329505243cb2f26d5cf84d9f8d0825ff8b35e9c999", size = 631445, upload-time = "2026-02-05T16:27:40.953Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/fe/64b3d035780b3188f86c4f6f1bc202e7bb74757ef028802112273b9dcacf/openai-2.31.0.tar.gz", hash = "sha256:43ca59a88fc973ad1848d86b98d7fac207e265ebbd1828b5e4bdfc85f79427a5", size = 684772, upload-time = "2026-04-08T21:01:41.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/97/284535aa75e6e84ab388248b5a323fc296b1f70530130dee37f7f4fbe856/openai-2.17.0-py3-none-any.whl", hash = "sha256:4f393fd886ca35e113aac7ff239bcd578b81d8f104f5aedc7d3693eb2af1d338", size = 1069524, upload-time = "2026-02-05T16:27:38.941Z" }, + { url = "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl", hash = "sha256:44e1344d87e56a493d649b17e2fac519d1368cbb0745f59f1957c4c26de50a0a", size = 1153479, upload-time = "2026-04-08T21:01:39.217Z" }, ] [[package]] @@ -2182,6 +2581,102 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -2191,6 +2686,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -2514,6 +3021,15 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -2557,6 +3073,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -2794,6 +3322,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "rich" +version = "14.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, +] + [[package]] name = "rpds-py" version = "0.30.0" @@ -2939,6 +3480,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -2948,6 +3507,15 @@ 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 = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.49" @@ -3006,6 +3574,18 @@ asyncio = [ { name = "greenlet" }, ] +[[package]] +name = "sqlite-vec" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" }, + { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" }, + { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" }, +] + [[package]] name = "sse-starlette" version = "3.2.0" @@ -3032,6 +3612,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] +[[package]] +name = "structlog" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, +] + +[[package]] +name = "tavily-python" +version = "0.7.23" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "requests" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d1/197419d6133643848514e5e84e8f41886e825b73bf91ae235a1595c964f5/tavily_python-0.7.23.tar.gz", hash = "sha256:3b92232e0e29ab68898b765f281bb4f2c650b02210b64affbc48e15292e96161", size = 25968, upload-time = "2026-03-09T19:17:32.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/27/f9c6e9249367be0772fb754849e03cbbc6ad8d80a479bf30ea8811828b2e/tavily_python-0.7.23-py3-none-any.whl", hash = "sha256:52ef85c44b926bce3f257570cd32bc1bd4db54666acf3105617f27411a59e188", size = 19079, upload-time = "2026-03-09T19:17:29.593Z" }, +] + [[package]] name = "tenacity" version = "9.1.4" @@ -3041,6 +3644,122 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] +[[package]] +name = "textual" +version = "8.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/2f/d44f0f12b3ddb1f0b88f7775652e99c6b5a43fd733badf4ce064bdbfef4a/textual-8.2.3.tar.gz", hash = "sha256:beea7b86b03b03558a2224f0cc35252e60ef8b0c4353b117b2f40972902d976a", size = 1848738, upload-time = "2026-04-05T09:12:45.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/28/a81d6ce9f4804818bd1231a9a6e4d56ea84ebbe8385c49591444f0234fa2/textual-8.2.3-py3-none-any.whl", hash = "sha256:5008ac581bebf1f6fa0520404261844a231e5715fdbddd10ca73916a3af48ca2", size = 724231, upload-time = "2026-04-05T09:12:48.747Z" }, +] + +[[package]] +name = "textual-autocomplete" +version = "4.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "textual" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/3a/80411bc7b94969eb116ad1b18db90f8dce8a1de441278c4a81fee55a27ca/textual_autocomplete-4.0.6.tar.gz", hash = "sha256:2ba2f0d767be4480ecacb3e4b130cf07340e033c3500fc424fed9125d27a4586", size = 97967, upload-time = "2025-09-24T21:19:20.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/66/ebe744d79c87f25a42d2654dddbd09462edd595f2ded715245a51a546461/textual_autocomplete-4.0.6-py3-none-any.whl", hash = "sha256:bff69c19386e2cbb4a007503b058dc37671d480a4fa2ddb3959c15ceb4aff9b5", size = 16499, upload-time = "2025-09-24T21:19:18.489Z" }, +] + +[[package]] +name = "textual-speedups" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/73/bba3e9feae9ca730c32122306ddac61278a8bc47633346eddad9d52a435d/textual_speedups-0.2.1.tar.gz", hash = "sha256:72cf0f7bdeede015367b59b70bcf724ba2c3080a8641ebc5eb94b36ad1536824", size = 10951, upload-time = "2025-11-28T09:38:51.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/b0/cc5bd0e7d5b4dc8ce2e43684fc04d0b258536fc99fdd98cbee781b325316/textual_speedups-0.2.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:27715265e1754b88d4a5600046b727c926423d0e63497d1f01a96deeca816955", size = 289456, upload-time = "2025-11-28T09:38:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5c/9901604eda5bffeb3c059deae94ebb9243d0dc573ac0753c9fc5bb8e7887/textual_speedups-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9bf2860116b73da0bb18b7a3b2559811b911b00686b2f7acd51cd15ced2c5f43", size = 281741, upload-time = "2025-11-28T09:37:55.463Z" }, + { url = "https://files.pythonhosted.org/packages/65/5a/1808f852aa9f8f6c1bcaa03e0a3cd7ad75a2d05d1782df4442df453b13bd/textual_speedups-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03543755f947aba5b8b85a19b9a22326b89ad4bddd072a96b504d197717ecf67", size = 314293, upload-time = "2025-11-28T09:36:51.924Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ae/26f2381f6cd1fcf95b59977cb14046516214927e9741c962876fef2adaa0/textual_speedups-0.2.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:267dbc7f3670e5bf41ac584082cff695984bf556430fa3b3ad67986610193520", size = 320306, upload-time = "2025-11-28T09:37:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2c/9a27c9996307bb3593b96b9e6679e92d724bbb33923cd8256400005ccd36/textual_speedups-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af8f588ceab3e1e15ba8577af93a31e83cda974d951d34c0ffcf02339ffb315e", size = 438478, upload-time = "2025-11-28T09:37:14.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/f8/551fb3efbdbf3ac5b27d885e50d48cbd6b4d3654634221055ddda9eb03a6/textual_speedups-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a283550fc07957fea15e2406648c1f7efd34521638f7b29f71ca1eebe9a84843", size = 335491, upload-time = "2025-11-28T09:37:27.192Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cd/7711a0724fe9cd8601bb1ddad4cf355ea594541af47e9dca82b5466ebdd9/textual_speedups-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fb7f4c0c8d42ca290c2e66473844c3d6a65fddb370823fa346adb0a4a0019ad", size = 313708, upload-time = "2025-11-28T09:37:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/43/38/81b2f02caec2f80a47da913817f687447a03ca8b50b04bae052cb29180fe/textual_speedups-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbd3e3253eafc092acc31e09b8330bc22cbf66553ef0e17d37581203237a670b", size = 338434, upload-time = "2025-11-28T09:37:38.247Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3f/77dcd1237f54b2a6e4e77d036d61799da770387bab9d155decc38ce3dbd0/textual_speedups-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2fba949af1e836a8419d80c391feae701286b606af646e26f068c023309b1dae", size = 496558, upload-time = "2025-11-28T09:38:06.023Z" }, + { url = "https://files.pythonhosted.org/packages/a5/12/a9a674b4361977bdf1aeb7cfc5d3574d500fd88e747a0aca751dea69b6bf/textual_speedups-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:25997f169325bc4b7b75086d066d322d136df928486a450fb4d90a1438a75fc6", size = 587583, upload-time = "2025-11-28T09:38:17.817Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/58cec72ef5611ae38c5b62111188537646c91c6d30b459ceb9f88449125a/textual_speedups-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5bffdbc74592302c6af3da36f4213d1263116b040251eae1c2c20e5f99b4afe", size = 517828, upload-time = "2025-11-28T09:38:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/db/d1/3d1233b41ca453f69ce7852a84a20028c13fde82241169abb62b09b756c3/textual_speedups-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca60b2fe2b9ac3d6aa08dde9879711b5befd0c3bfb8cc99aaa176087c5feae7f", size = 484157, upload-time = "2025-11-28T09:38:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/83/c4/ae35413e0bb255a69cc7727261af602fe5934e612c5926fd50b4691e802b/textual_speedups-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:3a4f402b2c1d0d778e96fee790024196295eebc2519d886fa262fdbf96455966", size = 162033, upload-time = "2025-11-28T09:38:53.45Z" }, + { url = "https://files.pythonhosted.org/packages/05/3d/acf24db3e1f3f218331da770d12ff93ad5d6021143aa7b2dfafff7f3940c/textual_speedups-0.2.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:84d2209fc85b7b06d81de200e46de6b51961e2ea1708cd568059983d7b7b634e", size = 284071, upload-time = "2025-11-28T09:38:01.425Z" }, + { url = "https://files.pythonhosted.org/packages/eb/30/0891458bcd91c36e7f54deb300a7691daf6003f8f7f696aa7a809a09c556/textual_speedups-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aabadeabdd03bce13b55a4c9c927d702160695c3f2f8a1789b95ed0102968a8d", size = 279385, upload-time = "2025-11-28T09:37:56.572Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/7b870f4dde1df3b2f3cf44147eda9996a8469dde32eb7a8df517943be82f/textual_speedups-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a47c888d9cfdc6b2785844393e250fb268b404ea8eee937ac862b7b1666e82", size = 312100, upload-time = "2025-11-28T09:36:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/b0/36/bc1acea15b42ecb7820dbeebf878fd11e5ce5ee2205cba23eb5db66bc193/textual_speedups-0.2.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d4c80c6e1d687d4d508de856a0c7c5090e07d964c09701b582cc1f64452f753", size = 317812, upload-time = "2025-11-28T09:37:05.329Z" }, + { url = "https://files.pythonhosted.org/packages/47/b7/9afee0b02706eae5a6ebb5a58e490b15b71e7259c3c730c1b08e4cc8c107/textual_speedups-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4523970dfd9d2718fdd293a8ae1d8b098ca65bdd54666416a13d5acbaacdc64a", size = 437546, upload-time = "2025-11-28T09:37:16.687Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/4cb41b47c9426e549b9e1510f0937eaeb9333618854ba3cb77e5c656fc5e/textual_speedups-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a3129288536be5e5319f3a03525a04e222c46340d448f180d2c4327b2651563", size = 333720, upload-time = "2025-11-28T09:37:28.244Z" }, + { url = "https://files.pythonhosted.org/packages/01/b9/a626b2ea864293bca999565ae0040315f0cd7ffb9147dfb30467de3ec1a0/textual_speedups-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5e43f0af17ef8834ecdf1c1be466eca441970547b5df2e6c17eea301290118d", size = 311174, upload-time = "2025-11-28T09:37:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/ed6a9ef7c357f95504a151d4df376eee6a24056bbdf7009bfe25815cc437/textual_speedups-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1b271489001115ef18dbf92e68cc5f081285a7758e63daef9894c83244b8700c", size = 335118, upload-time = "2025-11-28T09:37:39.452Z" }, + { url = "https://files.pythonhosted.org/packages/e9/54/396bdc7f9827a6d8abea2fffceb6f17e23dcb0f1e9ff5b6c102714b1ec6d/textual_speedups-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:68c3a20b2e7b8d33c292fcd8d613e6f2e0e25ff131d988c73646757f50a37f3f", size = 493537, upload-time = "2025-11-28T09:38:07.187Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/b7eb6f4366c19b7baeec3e9b8cc91d817c36f0c43f91a5e8ed158fccb6da/textual_speedups-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:50bcc9f315930ab0214ee87847f901fcdd71d2972d683502a495f6266489c208", size = 585045, upload-time = "2025-11-28T09:38:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/f7/82/1f21d68a711b178ba7e91517b7c920f97b7bac4dc7104da77c9e06bd0ded/textual_speedups-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7fede4ded345dc7205cf04e0157179bb37c5477d2d8193c1cbe2171256e63fdb", size = 514284, upload-time = "2025-11-28T09:38:31.791Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b5/971e3274d809a36a56bd17b8c2d908e3e9c11d02ccbc57a717b897325623/textual_speedups-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:70f07dc375244d6eb461df2405413b2d67cabf7265a255213e70dcaae1ae6ff9", size = 482176, upload-time = "2025-11-28T09:38:43.419Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4d/bf4e19bdffb137a4c2a027ffde32b77a1465da5a2c8470b0058b9a156486/textual_speedups-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:19f4d1b13da38ba0e7a6c87815447b114301670f9d4e6f9a7bdf9d4ae9f5f926", size = 159927, upload-time = "2025-11-28T09:38:54.56Z" }, + { url = "https://files.pythonhosted.org/packages/12/5b/485f122aad6a084ad4b95483078181565fe17c65b5420db13f744f8c984f/textual_speedups-0.2.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:55cc5a86ceb7cf82e0089a6d45daa3c3ea9248bb2a2fbcde40f9aeb562386a24", size = 284044, upload-time = "2025-11-28T09:38:02.899Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ab/af8eb2c53b65bd1f868109602ae11daac34d0889c80380b556f644793af5/textual_speedups-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c762c60275dce06b9c9bde4173cefef950798877da0aac46538ccd0d2ffb2f43", size = 279446, upload-time = "2025-11-28T09:37:57.822Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d8/aab66c0401118633ac3121d4f79695e86d37aae3b2ebd49a626d034ba64e/textual_speedups-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42172f4b7742988d9ff28098600b8a4a86a7e86ba39fcc3c779a93b98ef31abc", size = 311522, upload-time = "2025-11-28T09:36:54.59Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3f/3376f7caba0caecb643f7cede803fd4ba15b8262ea8b479201e3a6599768/textual_speedups-0.2.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2b58a100a3634e24bb532ad70602439bf358ea68687eb58eb18b05d699136e9", size = 316844, upload-time = "2025-11-28T09:37:06.52Z" }, + { url = "https://files.pythonhosted.org/packages/22/39/a6745c82a379e0a924f788e5a3ab7c29792c9d9847cec88e7f95d8c2c751/textual_speedups-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bded0beea2e344c0a2eea378235c61a8cab66e3eee8c5f6a92841b8c83d8b42", size = 438347, upload-time = "2025-11-28T09:37:17.828Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/f030a5e104faed6339d2be7dedcf8e880b463860fe5b881a52cdfb88e0a5/textual_speedups-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7a2cc60b3b87b684fdf9059f2f952dfc0a41e351018331862ddbb76b87f86a1", size = 334125, upload-time = "2025-11-28T09:37:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/38a83d71467dab3e4dff6423ce2daa57da67032cfd918a6f697e31e6807c/textual_speedups-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1389c87c4e1a071ba7ef0a2189d6989b753893ade56fea2e46bda5579f9e1844", size = 310870, upload-time = "2025-11-28T09:37:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/97/19/543c077c5b7164a66e98778ec37da322d6cb126f7873ab3523a7974f47cc/textual_speedups-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3760151da33f317f0d99d0191b1da1ccb1464e8d172207c857b6f25e67bb9ce2", size = 334639, upload-time = "2025-11-28T09:37:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/df/53/416919d1a944443d0b98e21c4d12c43f623e55b5faa27db45e62bf8a8fd8/textual_speedups-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c6f9b5a76e4df9614313be8b32192705f41c24b1fdac2a63e5d8fb8a82098f4", size = 492669, upload-time = "2025-11-28T09:38:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/68/dd/234f7c4aa735f7ab37f0c074b35f1a152822b2bc08d3ccee76a6af4b1dda/textual_speedups-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:472bfecd1de0fecdfb8ac27f0543988f54f9f9e7ec10e4bd9ec1817c9e97ebf2", size = 584269, upload-time = "2025-11-28T09:38:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ec/a9609d4a3b4bad84acc36eab8f8e59e9c8fe234341a57ced7679d784a3a2/textual_speedups-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:633ede114e6d24ea1fe99b6165242b233569008d0f4e30fb1e4484137dd6ec8a", size = 513963, upload-time = "2025-11-28T09:38:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/01/d9/dc46c490a16fae66a0ac4f6ae087937cc7644cd80a7820aafcdcfd73e4e1/textual_speedups-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:85704fe03f0df3314354815d5edcc58121003e25a4ecaab7d9ab3ab8f91e9abc", size = 482097, upload-time = "2025-11-28T09:38:44.56Z" }, + { url = "https://files.pythonhosted.org/packages/ff/47/fb88052dbb1e2dd9cd3a46bb235fe0e9dfb0423389dc19b99f517dfafae9/textual_speedups-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:f360211bde6e58e0e7c9b594ee317682ec2812d8f3613087421a42142ed69bdd", size = 159731, upload-time = "2025-11-28T09:38:55.557Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/0bbb3b4516ce0b6fa040b38483a1c442cfe8bcb9dfcf819063cf0e569d96/textual_speedups-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d132064312e9a79e44612ba227ddea071803e9f2bb725e17e2b96c2e2d707f8a", size = 312763, upload-time = "2025-11-28T09:36:55.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8d/fd65c4b5d720cbff4ac0666074b3de37bc05d594ea86ce946bfbc84fbaec/textual_speedups-0.2.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:40a409f9f1ada86f8ea25908bdfea0323122fbfc1283437b95541c91b05c6071", size = 319133, upload-time = "2025-11-28T09:37:07.649Z" }, + { url = "https://files.pythonhosted.org/packages/bf/23/406690d7b19bac5963976ade0419ebadba28f4c6818a13f4359f4ef71d2c/textual_speedups-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:487bd44791ccab54b13db1c2359380fb58e8c8aa2a3c7f976b80fe552060bf59", size = 441656, upload-time = "2025-11-28T09:37:19.32Z" }, + { url = "https://files.pythonhosted.org/packages/aa/40/fe309db81d77491a3f2f9b2648d13c2cb0de462f2ca1c12237d5359e5d2c/textual_speedups-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31fd676c50114df04ab3d84fc76741d6b8542db477d1887c5d5ef6a9d1c7d06b", size = 335020, upload-time = "2025-11-28T09:37:30.595Z" }, + { url = "https://files.pythonhosted.org/packages/31/11/2eb96221e278a5c51487d7489f88d5a70f01b3e0f6b3de243ff69e7b9ad4/textual_speedups-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:72fc01e98be7ab7bb251be6e7f7d5dbff409d9242ce729e0d7786268876f3e63", size = 494418, upload-time = "2025-11-28T09:38:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/098eed1da86a9897244ba07a23b2b7d94b506c9826ede7d67e5e365ba109/textual_speedups-0.2.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5c9d36c952f86857625833717ee49e7329685d435d6d0764b6998c6189a0c3d", size = 586312, upload-time = "2025-11-28T09:38:22.553Z" }, + { url = "https://files.pythonhosted.org/packages/cf/1b/67b399255357db398d2ea24f4e1dc9fb55f691546328b50f3ff8c26fb697/textual_speedups-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:476331a1180c57b49a4f553840b195a83bcd3a9ff2c199ad7baa975447d3db05", size = 516036, upload-time = "2025-11-28T09:38:34.275Z" }, + { url = "https://files.pythonhosted.org/packages/94/7d/5c05f0237ea0665dcfe88982b45148a64883cb78bbf5b6bb1beb66265bb2/textual_speedups-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bdd9298bd66853b2df37a766193d28bdc0de86b80067fd90737fd9405bd5a727", size = 483602, upload-time = "2025-11-28T09:38:45.728Z" }, + { url = "https://files.pythonhosted.org/packages/91/ca/b878beabe3ad2c4aa958f55cb32ba34e7badaa09f73c6e94c87195eb531e/textual_speedups-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:99a88f44c1b846d51dd115b5fb4dbf97bdef83e9f98a84efec16922a5974e230", size = 278485, upload-time = "2025-11-28T09:37:58.941Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/9b49ea67a5e9a6f54600deb63bd7c0fd7dbe4a3592300207d41c52762a1b/textual_speedups-0.2.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d2036752ff0df77972b157e71dd942fd75871d9c4feca751f222310bff7c17a", size = 311262, upload-time = "2025-11-28T09:36:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/81/88/6397efb6bf31c0c815ad12f63e84e3a3f952cab5b4dbdadad3a368ced436/textual_speedups-0.2.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:267c44974ea63e9742ea6cbb65f96d5dbdd09b2f9979823a4b7f664614dd21af", size = 317933, upload-time = "2025-11-28T09:37:08.745Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ce/e687d556e6a072b14a6c46ed647a9d5cf862df24c9639c17bd66c0655908/textual_speedups-0.2.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd3f19c6fd7148b6ba86c4c8bde7fdbf96b33687dc42872b8da2fdf67f5ab6d3", size = 438268, upload-time = "2025-11-28T09:37:20.429Z" }, + { url = "https://files.pythonhosted.org/packages/ec/06/e111d6d5e6a5f927ff3ee940c08d4b904143213a5dc2a325832f66848b9b/textual_speedups-0.2.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6245ea2c4883815ed23ecaf852e4de3d2d99e5f082790e780202705730c30b9a", size = 333978, upload-time = "2025-11-28T09:37:31.777Z" }, + { url = "https://files.pythonhosted.org/packages/b3/20/7030634f4e2c5f2c410cf8e6fbcad6e278d0a6dc7ead5fcf31d497951fce/textual_speedups-0.2.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aaade447724543c60e546542ff91224028992ce4a2a071c81b1927cead114b3", size = 310102, upload-time = "2025-11-28T09:37:51.982Z" }, + { url = "https://files.pythonhosted.org/packages/15/34/7b2b7d831d3668d27285e21c45ff03fab5c9b41092948995562517e53f8b/textual_speedups-0.2.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:821d3e57014d0d426c28fc8da4adaa0829f5cdf3ea133559ce6c64baf3aa98d6", size = 335335, upload-time = "2025-11-28T09:37:42.792Z" }, + { url = "https://files.pythonhosted.org/packages/49/90/909a6fd6cf29e323ae6102474ed254e037ac7d1c534e33860ecd54013d8a/textual_speedups-0.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fecfdc25439a890dfb9f7357f2d854ef801c5a6484b595dde1baf6a15d20b945", size = 492172, upload-time = "2025-11-28T09:38:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7f/2a50d7c712ad2b6495344bf9f1d0a805291b1cb53595e7ad7b749d253c29/textual_speedups-0.2.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0df559abaa14324757e8995e27c83171673377effd7348350f2d1b852eb0e8c1", size = 584840, upload-time = "2025-11-28T09:38:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a6/ad6b451b5fa550ed13e7084440b9d89bb689b8aa5afe1b26b83b49540057/textual_speedups-0.2.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ebc5094080c61d06c71011da6c7522e51635d1425bfa14d693bca6059e64b795", size = 514648, upload-time = "2025-11-28T09:38:35.774Z" }, + { url = "https://files.pythonhosted.org/packages/c2/45/3c81292f786b57dca7468da732dca902dc7ca1a280b0d785ee7c721f5dc4/textual_speedups-0.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5fee92c2e442b86644769011dfbd4f0dbcd43490eecd577735a09a751f3d4a31", size = 481640, upload-time = "2025-11-28T09:38:46.902Z" }, + { url = "https://files.pythonhosted.org/packages/a2/33/1894d2433d0e852177857c3a328346aae626d096e34a1064691ccf04eca1/textual_speedups-0.2.1-cp314-cp314-win32.whl", hash = "sha256:8ffcf6711869f4241a751aacc055f46c842fb633000964c94e3d1f1bf4b887c0", size = 151110, upload-time = "2025-11-28T09:38:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b3/0b3957b1187c657794d36529d7a0d7e81e2a8af6f68d0e0b57f53f347db1/textual_speedups-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:1889ae903263c47f76905443a5274d3cceacf5ee218af8c79ef24598c54ac70e", size = 159722, upload-time = "2025-11-28T09:38:56.653Z" }, + { url = "https://files.pythonhosted.org/packages/a2/67/0d79c23d74736b51a84a8bb2adcf538a4f4c01b681f916a2f6c12aebc9a1/textual_speedups-0.2.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b03ffba57b8d2eb991a2edcc4a873096228ab2c1c3ba8577e502204a721037c8", size = 312683, upload-time = "2025-11-28T09:36:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/67/d3/9b19fcaaf27799846d89a24b5d4e432425eb3ea4811e61a358685ebcde62/textual_speedups-0.2.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03a8dc93c983215e187c414a66e5d338c8805f30180b8b845c05914e37a1b315", size = 319134, upload-time = "2025-11-28T09:37:09.881Z" }, + { url = "https://files.pythonhosted.org/packages/44/18/fb492f764e756353e11ce82691eea04bd57d43e7eae86e96a53f49c8830d/textual_speedups-0.2.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3634667a035134cda18890a623f9fc561ff3c22d9a6e97c6244e52047251f454", size = 441148, upload-time = "2025-11-28T09:37:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/f3f8d27a85de6ba74a97b19f5174b44df9cf4d7188792c39017a0f872937/textual_speedups-0.2.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5786c2c56cf0e99ad89481f1464ec002d1bfe3adbd5bcb5f1f9b9fd03a7cc063", size = 335003, upload-time = "2025-11-28T09:37:32.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/53/e74500c99414d44565f1c26c906f382085ce98ba33c986b4ffede69102b9/textual_speedups-0.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d05bdd2c760833c9ed8d1fffb7cca2e293c15031e53e270e06766dbc96771fc", size = 494039, upload-time = "2025-11-28T09:38:12.511Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9a/b834df281395d5d3311a627ac8587e61f0e4bcdf53c1df451dbd5822e1e0/textual_speedups-0.2.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7dc58e59d740e66d1e0a9ce18835ee0e6a72a61bf0a4c1afce38273e217fdf27", size = 586432, upload-time = "2025-11-28T09:38:25.238Z" }, + { url = "https://files.pythonhosted.org/packages/6b/46/375d5b63a68a48770bf556ca364ef9d9cd79574f3b5dee2c542706177d28/textual_speedups-0.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e6e8c74a63080a50a95f26e87818c66d8507de21c6d610ed049f91b84402aafa", size = 516212, upload-time = "2025-11-28T09:38:36.935Z" }, + { url = "https://files.pythonhosted.org/packages/06/02/80c118df98fa8ef84fa8e690b18c20302dc697c59dfee8a9667b72424aa6/textual_speedups-0.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5fd85522114aec21c3915725992401e2bf6372ae5b1e3b2ebd85e1c3c7115885", size = 483699, upload-time = "2025-11-28T09:38:48.087Z" }, + { url = "https://files.pythonhosted.org/packages/f9/92/f8bb3c1c518e3eacc5f65929a365a2fdf239ce7e5d77102fa82dd3504715/textual_speedups-0.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c782c16e2c06633b1422837f22755f2d818ebd9140c6d048c1f065396798670f", size = 315656, upload-time = "2025-11-28T09:37:01.717Z" }, + { url = "https://files.pythonhosted.org/packages/de/e6/5aeba69ecaf0b9abea342fbe8ecca9bfde495d62e238e14db9a9126d1007/textual_speedups-0.2.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9df573f105703e368cd495d8d275b51b1f4595d24fa7ef90a5afe438843a2019", size = 320787, upload-time = "2025-11-28T09:37:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/beaa2d940bd17db5ebf57c7f3efe5d5ee0ac76ab14b0dc5c090856ba64c6/textual_speedups-0.2.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:828ce016c84e8af7038347b14deff94fb4cbd2153e6c89c3552bd560ca8bec91", size = 443291, upload-time = "2025-11-28T09:37:24.583Z" }, + { url = "https://files.pythonhosted.org/packages/63/b7/c2da63fc7e3603730d88357f02d1642153bda47bbd3132ba5c28828996cc/textual_speedups-0.2.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eaf0e7582c3f88a3c9dd18de725891f4e9977355459b643098fefbd36650b62", size = 337838, upload-time = "2025-11-28T09:37:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/e2/02/ec6eaf0057b51c975052182b6fc6f97ba739b6519c00d9b47a916ad7c2b1/textual_speedups-0.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5218771fa3273c3d1d49ce53077358b3d5d840acb1043d377b6674f45cd7c975", size = 314468, upload-time = "2025-11-28T09:37:54.425Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2a/b15ffb767ecb25e9401009e56836a3fa6e1adafd3ff0cd45b57d042fa292/textual_speedups-0.2.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:77fed425653027f4759f120124cc04c1a0143b30bb34172de9858c7d3c582379", size = 339472, upload-time = "2025-11-28T09:37:45.673Z" }, + { url = "https://files.pythonhosted.org/packages/e9/10/1da556e2fb12d8b3c9c37ac83683e99d8a4c309ffad891a40147e8550c41/textual_speedups-0.2.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a48fa24cd460ee4340a48f3e81a8299d282c2a23f7365e3bcf31ad7ab8385653", size = 497749, upload-time = "2025-11-28T09:38:14.895Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/06e5828b5943df72ef5efe90b5712bde4e52341b88c5ffa0264aedc04610/textual_speedups-0.2.1-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:f3e99ad237787f48f95a5764cd57c3d03818df07b06f38fc7bd5827d543c7f5b", size = 587933, upload-time = "2025-11-28T09:38:27.65Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7c/79b92e4c6692fbef79b388c8b233084f2fb662b52b8f489380b6e5bcd264/textual_speedups-0.2.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:721803e803ae1924aa18b0f108f0bfae41effa3c52d180e9ac05698785b9516c", size = 518798, upload-time = "2025-11-28T09:38:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/58/60/96b689f2f3ceb81296c430d36684abd23090b86630ca869f063fab943085/textual_speedups-0.2.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:074538c2478e5a0a40079c560ef1516b7567b2d2844e49c2f44974f24e5a5a77", size = 484872, upload-time = "2025-11-28T09:38:50.564Z" }, +] + [[package]] name = "tiktoken" version = "0.12.0" @@ -3149,6 +3868,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "tqdm" version = "4.67.3" @@ -3161,6 +3889,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -3182,6 +3919,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -3381,6 +4127,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, ] +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + [[package]] name = "websockets" version = "15.0.1"