From ef4b444d6fa693b7bd480b7b489d2f65fe273c2f Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Thu, 25 Jun 2026 10:30:10 +0200 Subject: [PATCH 1/4] feat: add MLflow tracing integration via OpenTelemetry bridge Add MLflow LLM observability support. Google ADK natively generates OTel traces; MLflow (>= 3.6.0) accepts them at its /v1/traces OTLP endpoint. The agent adds a second BatchSpanProcessor targeting MLflow alongside existing exporters. - config/settings.py: 6 new MLflow settings (enabled, tracking_uri, experiment_name, experiment_id, log_prompts, run_tags) - telemetry/setup.py: _add_mlflow_processor() sends all settings as OTLP headers; setup_telemetry() restructured so MLflow works independently of otel_enabled - pyproject.toml: mlflow optional dep group (otlp-proto-http exporter) - requirements-mlflow.txt: lock file with hashes - .env.example, Makefile: env var docs + lock-mlflow target - tests/test_mlflow.py: 26 test cases covering defaults, env loading, enabled/disabled paths, independent MLflow, headers, trailing slash handling, and ImportError Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 21 + Makefile | 17 +- pyproject.toml | 3 + requirements-mlflow.txt | 851 ++++++++++++++++++++++++ src/lightspeed_agent/config/settings.py | 26 + src/lightspeed_agent/telemetry/setup.py | 91 ++- tests/test_mlflow.py | 529 +++++++++++++++ 7 files changed, 1513 insertions(+), 25 deletions(-) create mode 100644 requirements-mlflow.txt create mode 100644 tests/test_mlflow.py diff --git a/.env.example b/.env.example index e4453134..949ad31d 100644 --- a/.env.example +++ b/.env.example @@ -284,3 +284,24 @@ OTEL_TRACES_SAMPLER=always_on # Sampler argument (e.g., 0.1 for 10% sampling with traceidratio) OTEL_TRACES_SAMPLER_ARG=1.0 + +# ----------------------------------------------------------------------------- +# MLflow Tracing Configuration (via OpenTelemetry bridge) +# ----------------------------------------------------------------------------- +# Enable MLflow tracing (works independently of OTEL_ENABLED; requires pip install 'lightspeed-agent[mlflow]') +# MLFLOW_ENABLED=false + +# MLflow tracking server URI +# MLFLOW_TRACKING_URI=http://localhost:5000 + +# MLflow experiment name +# MLFLOW_EXPERIMENT_NAME=lightspeed-agent + +# MLflow experiment ID (sent as x-mlflow-experiment-id header; overrides name if set) +# MLFLOW_EXPERIMENT_ID= + +# Enable logging of prompts and responses in MLflow traces +# MLFLOW_LOG_PROMPTS=false + +# Comma-separated key=value tags for MLflow runs (e.g., env=prod,team=ai) +# MLFLOW_RUN_TAGS= diff --git a/Makefile b/Makefile index e3a8f14a..ad5b90d5 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Red Hat Lightspeed Agent for Google Cloud - Makefile # Common development and deployment commands -.PHONY: help build build-agent build-marketplace run stop logs logs-mcp clean test test-shell lint dev check-env lock lock-agent lock-handler lock-dev lock-check audit +.PHONY: help build build-agent build-marketplace run stop logs logs-mcp clean test test-shell lint dev check-env lock lock-agent lock-handler lock-dev lock-mlflow lock-check audit # Default target help: @@ -18,6 +18,7 @@ help: @echo " make lock-agent - Regenerate agent lock file only" @echo " make lock-handler - Regenerate marketplace handler lock file only" @echo " make lock-dev - Regenerate dev lock file only" + @echo " make lock-mlflow - Regenerate mlflow lock file only" @echo " make lock-check - Verify lock files are in sync (used by CI)" @echo " make audit - Scan dependencies for known vulnerabilities (pip-audit)" @echo "" @@ -71,7 +72,7 @@ lint: # ============================================================================= lock: - $(MAKE) lock-agent lock-handler lock-dev + $(MAKE) lock-agent lock-handler lock-dev lock-mlflow lock-agent: @echo "Regenerating agent lock file..." @@ -91,6 +92,12 @@ lock-dev: --extra dev --output-file=requirements-dev.txt pyproject.toml @echo "✓ requirements-dev.txt updated" +lock-mlflow: + @echo "Regenerating mlflow lock file..." + source .venv/bin/activate && uv pip compile --generate-hashes --python-version=3.12 --python-platform=linux \ + --extra mlflow --output-file=requirements-mlflow.txt pyproject.toml + @echo "✓ requirements-mlflow.txt updated" + lock-check: @echo "Checking if lock files are in sync with pyproject.toml..." @cp requirements-agent.txt /tmp/requirements-agent-check.txt @@ -111,6 +118,12 @@ lock-check: @diff -u <(tail -n +3 requirements-dev.txt) <(tail -n +3 /tmp/requirements-dev-check.txt) || \ (echo "ERROR: requirements-dev.txt is out of sync. Run 'make lock' to update." && rm -f /tmp/requirements-dev-check.txt && exit 1) @rm -f /tmp/requirements-dev-check.txt + @cp requirements-mlflow.txt /tmp/requirements-mlflow-check.txt + @uv pip compile --generate-hashes --python-version=3.12 --python-platform=linux \ + --extra mlflow --output-file=/tmp/requirements-mlflow-check.txt pyproject.toml + @diff -u <(tail -n +3 requirements-mlflow.txt) <(tail -n +3 /tmp/requirements-mlflow-check.txt) || \ + (echo "ERROR: requirements-mlflow.txt is out of sync. Run 'make lock' to update." && rm -f /tmp/requirements-mlflow-check.txt && exit 1) + @rm -f /tmp/requirements-mlflow-check.txt @echo "✓ Lock files are in sync" audit: diff --git a/pyproject.toml b/pyproject.toml index 488675be..8ec6262f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,9 @@ telemetry = [ "opentelemetry-exporter-jaeger>=1.20.0", "opentelemetry-exporter-zipkin>=1.20.0", ] +mlflow = [ + "opentelemetry-exporter-otlp-proto-http>=1.20.0", +] dev = [ "pytest>=8.0.0", "pytest-asyncio>=0.23.0", diff --git a/requirements-mlflow.txt b/requirements-mlflow.txt new file mode 100644 index 00000000..bca6bc69 --- /dev/null +++ b/requirements-mlflow.txt @@ -0,0 +1,851 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes --python-version=3.12 --python-platform=linux --extra mlflow --output-file=requirements-mlflow.txt pyproject.toml +aiosqlite==0.22.1 \ + --hash=sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650 \ + --hash=sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb + # via lightspeed-agent (pyproject.toml) +annotated-doc==0.0.4 \ + --hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \ + --hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4 + # via fastapi +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via pydantic +anyio==4.14.1 \ + --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \ + --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e + # via + # httpx + # starlette +asyncpg==0.31.0 \ + --hash=sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8 \ + --hash=sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be \ + --hash=sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be \ + --hash=sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2 \ + --hash=sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d \ + --hash=sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a \ + --hash=sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7 \ + --hash=sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218 \ + --hash=sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d \ + --hash=sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602 \ + --hash=sha256:22be6e02381bab3101cd502d9297ac71e2f966c86e20e78caead9934c98a8af6 \ + --hash=sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab \ + --hash=sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095 \ + --hash=sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5 \ + --hash=sha256:37a58919cfef2448a920df00d1b2f821762d17194d0dbf355d6dde8d952c04f9 \ + --hash=sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9 \ + --hash=sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c \ + --hash=sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec \ + --hash=sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8 \ + --hash=sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047 \ + --hash=sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e \ + --hash=sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24 \ + --hash=sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31 \ + --hash=sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186 \ + --hash=sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3 \ + --hash=sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61 \ + --hash=sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a \ + --hash=sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1 \ + --hash=sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2 \ + --hash=sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2 \ + --hash=sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540 \ + --hash=sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c \ + --hash=sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8 \ + --hash=sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671 \ + --hash=sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad \ + --hash=sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d \ + --hash=sha256:bb223567dea5f47c45d347f2bde5486be8d9f40339f27217adb3fb1c3be51298 \ + --hash=sha256:bc2b685f400ceae428f79f78b58110470d7b4466929a7f78d455964b17ad1008 \ + --hash=sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3 \ + --hash=sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20 \ + --hash=sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2 \ + --hash=sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4 \ + --hash=sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109 \ + --hash=sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403 \ + --hash=sha256:c1a9c5b71d2371a2290bc93336cd05ba4ec781683cab292adbddc084f89443c6 \ + --hash=sha256:c1e1ab5bc65373d92dd749d7308c5b26fb2dc0fbe5d3bf68a32b676aa3bcd24a \ + --hash=sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b \ + --hash=sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735 \ + --hash=sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b \ + --hash=sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab \ + --hash=sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e \ + --hash=sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da \ + --hash=sha256:e6974f36eb9a224d8fb428bcf66bd411aa12cf57c2967463178149e73d4de366 \ + --hash=sha256:ebb3cde58321a1f89ce41812be3f2a98dddedc1e76d0838aba1d724f1e4e1a95 \ + --hash=sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d \ + --hash=sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44 \ + --hash=sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696 + # via lightspeed-agent (pyproject.toml) +certifi==2026.6.17 \ + --hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \ + --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db + # via + # httpcore + # httpx + # requests +cffi==2.0.0 \ + --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ + --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ + --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ + --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ + --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ + --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ + --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ + --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ + --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ + --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ + --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ + --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ + --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ + --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ + --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ + --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ + --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ + --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ + --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ + --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ + --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ + --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ + --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ + --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ + --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ + --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ + --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ + --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ + --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ + --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ + --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ + --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ + --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ + --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ + --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ + --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ + --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ + --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ + --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ + --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ + --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ + --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ + --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ + --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ + --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ + --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ + --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ + --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ + --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ + --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ + --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ + --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ + --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ + --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ + --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ + --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ + --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ + --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ + --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ + --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ + --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ + --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ + --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ + --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ + --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ + --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ + --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ + --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ + --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ + --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ + --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ + --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ + --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ + --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ + --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ + --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ + --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ + --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ + --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ + --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ + --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ + --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ + --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ + --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf + # via cryptography +charset-normalizer==3.4.7 \ + --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ + --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ + --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \ + --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ + --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ + --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ + --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ + --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \ + --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \ + --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \ + --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \ + --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \ + --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ + --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ + --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ + --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \ + --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \ + --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ + --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ + --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \ + --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ + --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \ + --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ + --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ + --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ + --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \ + --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ + --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \ + --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ + --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \ + --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ + --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ + --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \ + --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ + --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ + --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \ + --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \ + --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ + --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ + --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ + --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \ + --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ + --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \ + --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ + --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \ + --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ + --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ + --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \ + --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \ + --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \ + --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ + --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ + --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \ + --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \ + --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ + --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ + --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ + --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \ + --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ + --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ + --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ + --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ + --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \ + --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \ + --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \ + --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \ + --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \ + --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ + --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \ + --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ + --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \ + --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ + --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \ + --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ + --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ + --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ + --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \ + --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ + --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \ + --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ + --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \ + --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \ + --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \ + --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ + --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \ + --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \ + --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ + --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \ + --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ + --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \ + --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ + --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ + --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ + --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \ + --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \ + --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ + --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ + --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \ + --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \ + --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \ + --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ + --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \ + --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ + --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \ + --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \ + --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ + --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ + --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ + --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \ + --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \ + --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ + --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ + --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \ + --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ + --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ + --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \ + --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ + --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ + --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \ + --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ + --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ + --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ + --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ + --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ + --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 + # via requests +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 + # via uvicorn +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b + # via + # lightspeed-agent (pyproject.toml) + # google-auth + # pyjwt +fastapi==0.138.0 \ + --hash=sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0 \ + --hash=sha256:d445a4877636ad191e7053e08c9bf98cb921a6756776848400bb773d1740c061 + # via lightspeed-agent (pyproject.toml) +google-auth==2.55.0 \ + --hash=sha256:a17cef9dedf98c4ebae2fb0c48c8f75952c877cbc2efe09f329ef16c2783d88a \ + --hash=sha256:fcd3a130f575fa36403d38774af1c64a4fbfbca09215f0589d2372b5119697cb + # via lightspeed-agent (pyproject.toml) +googleapis-common-protos==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed + # via opentelemetry-exporter-otlp-proto-http +greenlet==3.5.2 \ + --hash=sha256:01e32e9d2b1714a2b06184cb3071ff2a2fd9bc7d065e39198ab21f7253dad421 \ + --hash=sha256:0488ca77c94da5e09d1d9958f98b58cebba1b8fd9664c24898499133de927574 \ + --hash=sha256:049827baab63dda8ab8ec5a6d07fc6eb0f418319cfc757fc8737a605e99ca1ad \ + --hash=sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34 \ + --hash=sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473 \ + --hash=sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32 \ + --hash=sha256:120b77c2a18ebf629c3a7886f68c6d01e065654844ad468f15bb93ace66f2094 \ + --hash=sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4 \ + --hash=sha256:1724499fc08388208408681c53c5062e9803c334e5a0bdaeb616228ba882aac8 \ + --hash=sha256:1adc23c50f22b0f5979521909a8360ab4a3d3bef8b641ce633a04cf1b1c967ea \ + --hash=sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb \ + --hash=sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8 \ + --hash=sha256:1f052fff492c52fdfa99bd3b3c1389a53de37dae76a0562741417f0d018f02b3 \ + --hash=sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682 \ + --hash=sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163 \ + --hash=sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c \ + --hash=sha256:2c6d6bfa4fdd7c39a0dbf112cdf28edbd19c517c810eefb6e4e71b0d55933a4c \ + --hash=sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f \ + --hash=sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c \ + --hash=sha256:30252d191d6959df1d040b559a38fc017139606c5ecc2ad00416557c0355d742 \ + --hash=sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32 \ + --hash=sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de \ + --hash=sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904 \ + --hash=sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e \ + --hash=sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a \ + --hash=sha256:423167363c510a75b649f5cd58d873c29498ea03598b9e4b1c3b73e0f899f3d5 \ + --hash=sha256:4e554809538bd4867f24421b43abde170f9c9b8192149b30df5e164bcac6124f \ + --hash=sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef \ + --hash=sha256:561dd919c02236a613fbf226791cbd77ee5002cbd5cb7e838869aa3ac7a71e16 \ + --hash=sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8 \ + --hash=sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b \ + --hash=sha256:5eba55076d79e8a5176e6925295cfb901ebc95dae493342ede22230f75d8bee2 \ + --hash=sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9 \ + --hash=sha256:6d9e19257794e28821c9ebd5e23f86d7c267cd9d390089374f068d2049f949e3 \ + --hash=sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d \ + --hash=sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f \ + --hash=sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39 \ + --hash=sha256:711028c953cd6ce5dc01bbb5a1747e3ad6bd8b2f7ded73778bb936e8dab9e3b6 \ + --hash=sha256:76dae33e97b52743a19210931ee3e78a88fe1438bc2fc4ee5e7512d289bfad4f \ + --hash=sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9 \ + --hash=sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce \ + --hash=sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00 \ + --hash=sha256:87359c23eb4e8f1b16da68faad29bf5aeb80e3628d7d8e4aa2e41c36879ddedd \ + --hash=sha256:89da99ee8345b458ea2f16831dad31c88ddcdec454b48704d569a0b8fb28f146 \ + --hash=sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3 \ + --hash=sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c \ + --hash=sha256:9dc23f0e5ad76415457212a4b947d22ebe4dc80baf02adf7dd5647a90f38bb4e \ + --hash=sha256:9df9daae96848508450011d0d86ed7c95f8829a354ce438284a77b24896fd1f8 \ + --hash=sha256:9e194b996aa1b89d933cfe136e5eb39b22a8b72ba59d376ef39a55bca4dbf47f \ + --hash=sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb \ + --hash=sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5 \ + --hash=sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3 \ + --hash=sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317 \ + --hash=sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a \ + --hash=sha256:a3f76a94e2d6e1fee8f302265679d8cc47d71a203936dd03c6e2ace0f9cfd46d \ + --hash=sha256:a850f6224088ef7dcc70f1a545cb6b3d119c35d6dca63b925b9f35da0635cdad \ + --hash=sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014 \ + --hash=sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0 \ + --hash=sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1 \ + --hash=sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e \ + --hash=sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5 \ + --hash=sha256:bc18b8d33e6976804b9b792fe11cb3b1fee8b646e8a9e20bf521a429ddf73520 \ + --hash=sha256:bf493b3c1c0a2324c49b0472e2280ba4665f3510d8115f6f807759a6163b15f7 \ + --hash=sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a \ + --hash=sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c \ + --hash=sha256:c1c1e5ad80f1f38ea479b83b39dccb20874cfe9ad5e52f87225fa294ba4d39a1 \ + --hash=sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73 \ + --hash=sha256:ca92411942154023c65851e6077d8ca0d00f19de5fa80bb2c6f196ff6c920ba9 \ + --hash=sha256:d7792398872f89466c6671d5d193537eff163ecf7fac78d82e6ddc25017fb4f5 \ + --hash=sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f \ + --hash=sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95 \ + --hash=sha256:e063263ce9047878480d7e536012fc8b7c8e1922989eb5f03b9ab998a2ee7b7e \ + --hash=sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c \ + --hash=sha256:e976f9f6941f57d87a194c91868622c8b22a142a741d2fde31655c319133ade6 \ + --hash=sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7 \ + --hash=sha256:f4d67c1684db3f9782c37ee4bade3f86f5a23a8fcf3f8359224106018ca40728 \ + --hash=sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4 \ + --hash=sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74 \ + --hash=sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c + # via sqlalchemy +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via + # httpcore + # uvicorn +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via lightspeed-agent (pyproject.toml) +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via + # anyio + # httpx + # requests +opentelemetry-api==1.43.0 \ + --hash=sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1 \ + --hash=sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd + # via + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-exporter-otlp-proto-common==1.43.0 \ + --hash=sha256:123c3f9cc87218562490c63b36f497bf3a722faf174a515d1443f31ababa6264 \ + --hash=sha256:c4e32ba6d6b13bdb2b8f6764c4fd28d00192826561aa04f6d14eedfce7ac076f + # via opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-http==1.43.0 \ + --hash=sha256:647f603aa8efdbdb4dbff842e0729d0406a6fff26b295a72d3d60e7d963b2610 \ + --hash=sha256:fa8a42bb7d00ee5391f4c0b04d8e6a46c03caa437903296ab73a81dc11ba118f + # via lightspeed-agent (pyproject.toml) +opentelemetry-proto==1.43.0 \ + --hash=sha256:224778df17e1f3fafeaaa21d874236ca5f6ffc2f86e0899298ec7351aac27924 \ + --hash=sha256:c58f1f7ef84bc7dc2834016c0c37fe0081dde7ca9f6339be1970fbf9cdaaa90d + # via + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.43.0 \ + --hash=sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823 \ + --hash=sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9 + # via opentelemetry-exporter-otlp-proto-http +opentelemetry-semantic-conventions==0.64b0 \ + --hash=sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c \ + --hash=sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6 + # via opentelemetry-sdk +protobuf==6.33.6 \ + --hash=sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326 \ + --hash=sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901 \ + --hash=sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3 \ + --hash=sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a \ + --hash=sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135 \ + --hash=sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e \ + --hash=sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3 \ + --hash=sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2 \ + --hash=sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593 \ + --hash=sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf + # via + # googleapis-common-protos + # opentelemetry-proto +psycopg2-binary==2.9.12 \ + --hash=sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f \ + --hash=sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964 \ + --hash=sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c \ + --hash=sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2 \ + --hash=sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115 \ + --hash=sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c \ + --hash=sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be \ + --hash=sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6 \ + --hash=sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c \ + --hash=sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c \ + --hash=sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c \ + --hash=sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d \ + --hash=sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019 \ + --hash=sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7 \ + --hash=sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3 \ + --hash=sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777 \ + --hash=sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd \ + --hash=sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5 \ + --hash=sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39 \ + --hash=sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c \ + --hash=sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf \ + --hash=sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b \ + --hash=sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433 \ + --hash=sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d \ + --hash=sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4 \ + --hash=sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290 \ + --hash=sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2 \ + --hash=sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3 \ + --hash=sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94 \ + --hash=sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d \ + --hash=sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b \ + --hash=sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965 \ + --hash=sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9 \ + --hash=sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f \ + --hash=sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354 \ + --hash=sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433 \ + --hash=sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9 \ + --hash=sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463 \ + --hash=sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5 \ + --hash=sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be \ + --hash=sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580 \ + --hash=sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4 \ + --hash=sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f \ + --hash=sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1 \ + --hash=sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915 \ + --hash=sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033 \ + --hash=sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03 \ + --hash=sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe \ + --hash=sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326 \ + --hash=sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0 \ + --hash=sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e \ + --hash=sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86 \ + --hash=sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5 \ + --hash=sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e \ + --hash=sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06 \ + --hash=sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936 \ + --hash=sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03 \ + --hash=sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56 \ + --hash=sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6 \ + --hash=sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256 \ + --hash=sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8 \ + --hash=sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab \ + --hash=sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980 \ + --hash=sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10 \ + --hash=sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a \ + --hash=sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2 \ + --hash=sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e + # via lightspeed-agent (pyproject.toml) +pyasn1==0.6.3 \ + --hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \ + --hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde + # via pyasn1-modules +pyasn1-modules==0.4.2 \ + --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ + --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 + # via google-auth +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + # via cffi +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 + # via + # lightspeed-agent (pyproject.toml) + # fastapi + # pydantic-settings +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae + # via pydantic +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f + # via lightspeed-agent (pyproject.toml) +pyjwt==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + # via lightspeed-agent (pyproject.toml) +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 + # via pydantic-settings +python-json-logger==4.1.0 \ + --hash=sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2 \ + --hash=sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195 + # via lightspeed-agent (pyproject.toml) +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 + # via lightspeed-agent (pyproject.toml) +redis==8.0.1 \ + --hash=sha256:47daa35a058c23468d6437f17a8c76882cb316b838ef763036af99b96cedd743 \ + --hash=sha256:afc5a7a2f5a084f5b1880dec548dd45be17db7e43c82a30d84f952aefb05cfb0 + # via lightspeed-agent (pyproject.toml) +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via + # lightspeed-agent (pyproject.toml) + # opentelemetry-exporter-otlp-proto-http +sqlalchemy==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via lightspeed-agent (pyproject.toml) +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 + # via + # lightspeed-agent (pyproject.toml) + # fastapi +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via + # anyio + # fastapi + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # pydantic + # pydantic-core + # sqlalchemy + # starlette + # typing-inspection +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 + # via + # fastapi + # pydantic + # pydantic-settings +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests +uvicorn==0.49.0 \ + --hash=sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f \ + --hash=sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3 + # via lightspeed-agent (pyproject.toml) diff --git a/src/lightspeed_agent/config/settings.py b/src/lightspeed_agent/config/settings.py index cea14a8c..53be9585 100644 --- a/src/lightspeed_agent/config/settings.py +++ b/src/lightspeed_agent/config/settings.py @@ -656,6 +656,32 @@ def _validate_session_backend(self) -> "Settings": description="DB polling interval in seconds for metrics collection", ) + # MLflow Tracing Configuration (via OpenTelemetry bridge) + mlflow_enabled: bool = Field( + default=False, + description="Enable MLflow tracing via OpenTelemetry span export", + ) + mlflow_tracking_uri: str = Field( + default="http://localhost:5000", + description="MLflow tracking server URI", + ) + mlflow_experiment_name: str = Field( + default="lightspeed-agent", + description="MLflow experiment name", + ) + mlflow_experiment_id: str = Field( + default="", + description="MLflow experiment ID (sent as x-mlflow-experiment-id header)", + ) + mlflow_log_prompts: bool = Field( + default=False, + description="Enable logging of prompts and responses in MLflow traces", + ) + mlflow_run_tags: str = Field( + default="", + description="Comma-separated key=value tags for MLflow runs (e.g., env=prod,team=ai)", + ) + @lru_cache def get_settings() -> Settings: diff --git a/src/lightspeed_agent/telemetry/setup.py b/src/lightspeed_agent/telemetry/setup.py index 99464599..84e2c9e5 100644 --- a/src/lightspeed_agent/telemetry/setup.py +++ b/src/lightspeed_agent/telemetry/setup.py @@ -155,15 +155,21 @@ def _create_meter_provider(resource: Resource, settings: Any) -> MeterProvider: def setup_telemetry() -> None: - """Initialize OpenTelemetry tracing and/or metrics.""" + """Initialize OpenTelemetry tracing, metrics, and/or MLflow span export. + + All three features are independently toggleable: + - otel_enabled: standard OTel tracing (Jaeger/Zipkin/OTLP) + instrumentation + - otel_metrics_enabled: OTel metrics with Prometheus + OTLP export + - mlflow_enabled: span export to MLflow's OTLP endpoint (works with or without otel_enabled) + """ global _tracer_provider, _meter_provider from lightspeed_agent.config import get_settings settings = get_settings() - if not settings.otel_enabled and not settings.otel_metrics_enabled: - logger.debug("OpenTelemetry tracing and metrics are disabled") + if not settings.otel_enabled and not settings.otel_metrics_enabled and not settings.mlflow_enabled: + logger.debug("OpenTelemetry tracing, metrics, and MLflow are disabled") return resource = Resource.create( @@ -174,32 +180,34 @@ def setup_telemetry() -> None: } ) - # Tracing setup - if settings.otel_enabled: - logger.info( - "Initializing OpenTelemetry tracing (service=%s, exporter=%s, sampler=%s)", - settings.otel_service_name, - settings.otel_exporter_type, - settings.otel_traces_sampler, - ) - + # Tracing setup (needed for both otel_enabled and mlflow_enabled) + if settings.otel_enabled or settings.mlflow_enabled: sampler = _get_sampler(settings.otel_traces_sampler, settings.otel_traces_sampler_arg) _tracer_provider = TracerProvider(resource=resource, sampler=sampler) - exporter = _create_exporter( - settings.otel_exporter_type, - settings.otel_exporter_otlp_endpoint, - settings.otel_exporter_otlp_http_endpoint, - ) - span_processor = BatchSpanProcessor(exporter) - _tracer_provider.add_span_processor(span_processor) + if settings.otel_enabled: + logger.info( + "Initializing OpenTelemetry tracing (service=%s, exporter=%s, sampler=%s)", + settings.otel_service_name, + settings.otel_exporter_type, + settings.otel_traces_sampler, + ) + exporter = _create_exporter( + settings.otel_exporter_type, + settings.otel_exporter_otlp_endpoint, + settings.otel_exporter_otlp_http_endpoint, + ) + _tracer_provider.add_span_processor(BatchSpanProcessor(exporter)) - trace.set_tracer_provider(_tracer_provider) + if settings.mlflow_enabled: + _add_mlflow_processor(settings, _tracer_provider) - _instrument_fastapi() - _instrument_httpx() + trace.set_tracer_provider(_tracer_provider) - logger.info("OpenTelemetry tracing initialized successfully") + if settings.otel_enabled: + _instrument_fastapi() + _instrument_httpx() + logger.info("OpenTelemetry tracing initialized successfully") # Metrics setup (independent of tracing) if settings.otel_metrics_enabled: @@ -241,6 +249,43 @@ def _instrument_httpx() -> None: logger.warning("Failed to instrument HTTPX: %s", e) +def _add_mlflow_processor(settings: Any, provider: TracerProvider) -> None: + """Add an OTLP HTTP span processor that exports traces to MLflow.""" + try: + from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter as OTLPHttpSpanExporter, + ) + + tracking_uri = settings.mlflow_tracking_uri.rstrip("/") + + mlflow_headers: dict[str, str] = {} + if settings.mlflow_experiment_id: + mlflow_headers["x-mlflow-experiment-id"] = settings.mlflow_experiment_id + if settings.mlflow_experiment_name: + mlflow_headers["x-mlflow-experiment-name"] = settings.mlflow_experiment_name + if settings.mlflow_log_prompts: + mlflow_headers["x-mlflow-log-prompts"] = "true" + if settings.mlflow_run_tags: + mlflow_headers["x-mlflow-run-tags"] = settings.mlflow_run_tags + + mlflow_exporter = OTLPHttpSpanExporter( + endpoint=f"{tracking_uri}/v1/traces", + headers=mlflow_headers, + ) + provider.add_span_processor(BatchSpanProcessor(mlflow_exporter)) + + logger.info( + "MLflow tracing enabled (endpoint=%s/v1/traces)", + tracking_uri, + ) + except ImportError: + logger.error( + "MLflow tracing requires opentelemetry-exporter-otlp-proto-http. " + "Install with: pip install 'lightspeed-agent[mlflow]'" + ) + raise + + def shutdown_telemetry() -> None: """Shutdown OpenTelemetry and flush any pending spans/metrics.""" global _tracer_provider, _meter_provider diff --git a/tests/test_mlflow.py b/tests/test_mlflow.py new file mode 100644 index 00000000..516d7241 --- /dev/null +++ b/tests/test_mlflow.py @@ -0,0 +1,529 @@ +"""Tests for MLflow tracing integration via OpenTelemetry bridge.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from lightspeed_agent.config import Settings +from lightspeed_agent.config.settings import get_settings + + +class TestMlflowSettingsDefaults: + """Verify all MLflow settings have correct default values.""" + + def test_mlflow_enabled_defaults_to_false(self): + """mlflow_enabled defaults to False.""" + settings = Settings() + assert settings.mlflow_enabled is False + + def test_mlflow_tracking_uri_default(self): + """mlflow_tracking_uri defaults to http://localhost:5000.""" + settings = Settings() + assert settings.mlflow_tracking_uri == "http://localhost:5000" + + def test_mlflow_experiment_name_default(self): + """mlflow_experiment_name defaults to 'lightspeed-agent'.""" + settings = Settings() + assert settings.mlflow_experiment_name == "lightspeed-agent" + + def test_mlflow_experiment_id_default(self): + """mlflow_experiment_id defaults to empty string.""" + settings = Settings() + assert settings.mlflow_experiment_id == "" + + def test_mlflow_log_prompts_defaults_to_false(self): + """mlflow_log_prompts defaults to False.""" + settings = Settings() + assert settings.mlflow_log_prompts is False + + def test_mlflow_run_tags_default(self): + """mlflow_run_tags defaults to empty string.""" + settings = Settings() + assert settings.mlflow_run_tags == "" + + +class TestMlflowSettingsFromEnv: + """Verify MLflow settings can be loaded from environment variables.""" + + def test_mlflow_enabled_from_env(self, monkeypatch): + """MLFLOW_ENABLED env var sets mlflow_enabled.""" + monkeypatch.setenv("MLFLOW_ENABLED", "true") + get_settings.cache_clear() + settings = Settings() + assert settings.mlflow_enabled is True + + def test_mlflow_tracking_uri_from_env(self, monkeypatch): + """MLFLOW_TRACKING_URI env var sets mlflow_tracking_uri.""" + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.example.com:5000") + get_settings.cache_clear() + settings = Settings() + assert settings.mlflow_tracking_uri == "http://mlflow.example.com:5000" + + def test_mlflow_experiment_name_from_env(self, monkeypatch): + """MLFLOW_EXPERIMENT_NAME env var sets mlflow_experiment_name.""" + monkeypatch.setenv("MLFLOW_EXPERIMENT_NAME", "my-experiment") + get_settings.cache_clear() + settings = Settings() + assert settings.mlflow_experiment_name == "my-experiment" + + def test_mlflow_experiment_id_from_env(self, monkeypatch): + """MLFLOW_EXPERIMENT_ID env var sets mlflow_experiment_id.""" + monkeypatch.setenv("MLFLOW_EXPERIMENT_ID", "42") + get_settings.cache_clear() + settings = Settings() + assert settings.mlflow_experiment_id == "42" + + def test_mlflow_log_prompts_from_env(self, monkeypatch): + """MLFLOW_LOG_PROMPTS env var sets mlflow_log_prompts.""" + monkeypatch.setenv("MLFLOW_LOG_PROMPTS", "true") + get_settings.cache_clear() + settings = Settings() + assert settings.mlflow_log_prompts is True + + def test_mlflow_run_tags_from_env(self, monkeypatch): + """MLFLOW_RUN_TAGS env var sets mlflow_run_tags.""" + monkeypatch.setenv("MLFLOW_RUN_TAGS", "env=prod,team=ai") + get_settings.cache_clear() + settings = Settings() + assert settings.mlflow_run_tags == "env=prod,team=ai" + + +class TestMlflowDisabled: + """Verify no MLflow exporter is added when mlflow_enabled=False.""" + + def test_no_mlflow_processor_when_disabled(self, monkeypatch): + """When mlflow_enabled=False, setup_telemetry adds no MLflow processor.""" + monkeypatch.setenv("OTEL_ENABLED", "true") + monkeypatch.setenv("OTEL_EXPORTER_TYPE", "console") + monkeypatch.setenv("MLFLOW_ENABLED", "false") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + with ( + patch.object(telemetry_mod, "_instrument_fastapi"), + patch.object(telemetry_mod, "_instrument_httpx"), + patch( + "opentelemetry.sdk.trace.TracerProvider.add_span_processor" + ) as mock_add_processor, + ): + telemetry_mod.setup_telemetry() + + # Only one span processor: the primary exporter (console) + assert mock_add_processor.call_count == 1 + + # Cleanup + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + def test_no_processors_when_both_disabled(self, monkeypatch): + """When both otel_enabled=False and mlflow_enabled=False, no processors at all.""" + monkeypatch.setenv("OTEL_ENABLED", "false") + monkeypatch.setenv("MLFLOW_ENABLED", "false") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + with patch( + "opentelemetry.sdk.trace.TracerProvider.add_span_processor" + ) as mock_add_processor: + telemetry_mod.setup_telemetry() + mock_add_processor.assert_not_called() + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + +class TestMlflowIndependent: + """Verify MLflow works independently of otel_enabled.""" + + def test_mlflow_only_creates_provider_and_processor(self, monkeypatch): + """When mlflow_enabled=True but otel_enabled=False, MLflow still gets a processor.""" + monkeypatch.setenv("OTEL_ENABLED", "false") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + + with ( + patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ), + patch( + "opentelemetry.sdk.trace.TracerProvider.add_span_processor" + ) as mock_add_processor, + ): + telemetry_mod.setup_telemetry() + + # Only MLflow processor (no OTel exporter since otel_enabled=False) + assert mock_add_processor.call_count == 1 + mock_http_exporter_cls.assert_called_once() + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + def test_mlflow_only_no_fastapi_instrumentation(self, monkeypatch): + """When only mlflow_enabled, FastAPI/HTTPX instrumentation is skipped.""" + monkeypatch.setenv("OTEL_ENABLED", "false") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + with ( + patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + MagicMock(), + ), + patch.object(telemetry_mod, "_instrument_fastapi") as mock_fastapi, + patch.object(telemetry_mod, "_instrument_httpx") as mock_httpx, + ): + telemetry_mod.setup_telemetry() + + mock_fastapi.assert_not_called() + mock_httpx.assert_not_called() + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + +class TestMlflowEnabled: + """Verify MLflow exporter is added when mlflow_enabled=True.""" + + def test_mlflow_processor_added_when_enabled(self, monkeypatch): + """When mlflow_enabled=True, setup_telemetry adds a second BatchSpanProcessor.""" + monkeypatch.setenv("OTEL_ENABLED", "true") + monkeypatch.setenv("OTEL_EXPORTER_TYPE", "console") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + mock_http_exporter_instance = MagicMock() + mock_http_exporter_cls.return_value = mock_http_exporter_instance + + with ( + patch.object(telemetry_mod, "_instrument_fastapi"), + patch.object(telemetry_mod, "_instrument_httpx"), + patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ), + patch( + "opentelemetry.sdk.trace.TracerProvider.add_span_processor" + ) as mock_add_processor, + ): + telemetry_mod.setup_telemetry() + + # Two processors: primary exporter + MLflow exporter + assert mock_add_processor.call_count == 2 + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + def test_mlflow_exporter_targets_tracking_uri(self, monkeypatch): + """MLflow OTLPSpanExporter is pointed at {tracking_uri}/v1/traces.""" + monkeypatch.setenv("OTEL_ENABLED", "true") + monkeypatch.setenv("OTEL_EXPORTER_TYPE", "console") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + monkeypatch.setenv("MLFLOW_EXPERIMENT_ID", "") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + + with ( + patch.object(telemetry_mod, "_instrument_fastapi"), + patch.object(telemetry_mod, "_instrument_httpx"), + patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ), + ): + telemetry_mod.setup_telemetry() + + mock_http_exporter_cls.assert_called_once() + call_kwargs = mock_http_exporter_cls.call_args + # Endpoint should be tracking_uri + /v1/traces + endpoint = call_kwargs.kwargs.get( + "endpoint", call_kwargs.args[0] if call_kwargs.args else None + ) + assert endpoint == "http://mlflow.local:5000/v1/traces" + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + +class TestMlflowExperimentIdHeader: + """Verify x-mlflow-experiment-id header is passed when experiment_id is set.""" + + def test_experiment_id_header_when_set(self, monkeypatch): + """When mlflow_experiment_id is set, OTLPSpanExporter gets the header.""" + monkeypatch.setenv("OTEL_ENABLED", "true") + monkeypatch.setenv("OTEL_EXPORTER_TYPE", "console") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + monkeypatch.setenv("MLFLOW_EXPERIMENT_ID", "42") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + + with ( + patch.object(telemetry_mod, "_instrument_fastapi"), + patch.object(telemetry_mod, "_instrument_httpx"), + patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ), + ): + telemetry_mod.setup_telemetry() + + mock_http_exporter_cls.assert_called_once() + call_kwargs = mock_http_exporter_cls.call_args + headers = call_kwargs.kwargs.get("headers", {}) + assert "x-mlflow-experiment-id" in headers + assert headers["x-mlflow-experiment-id"] == "42" + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + +class TestMlflowNoExperimentId: + """Verify no experiment-id header when mlflow_experiment_id is empty.""" + + def test_no_experiment_id_header_when_empty(self, monkeypatch): + """When mlflow_experiment_id is empty, no x-mlflow-experiment-id header.""" + monkeypatch.setenv("OTEL_ENABLED", "true") + monkeypatch.setenv("OTEL_EXPORTER_TYPE", "console") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + monkeypatch.setenv("MLFLOW_EXPERIMENT_ID", "") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + + with ( + patch.object(telemetry_mod, "_instrument_fastapi"), + patch.object(telemetry_mod, "_instrument_httpx"), + patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ), + ): + telemetry_mod.setup_telemetry() + + mock_http_exporter_cls.assert_called_once() + call_kwargs = mock_http_exporter_cls.call_args + headers = call_kwargs.kwargs.get("headers", {}) + assert "x-mlflow-experiment-id" not in headers + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + +class TestMlflowExperimentNameHeader: + """Verify x-mlflow-experiment-name header is passed when experiment_name is set.""" + + def test_experiment_name_header_when_set(self, monkeypatch): + """When mlflow_experiment_name is set, OTLPSpanExporter gets the header.""" + monkeypatch.setenv("OTEL_ENABLED", "false") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + monkeypatch.setenv("MLFLOW_EXPERIMENT_NAME", "my-experiment") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + + with patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ): + telemetry_mod.setup_telemetry() + + call_kwargs = mock_http_exporter_cls.call_args + headers = call_kwargs.kwargs.get("headers", {}) + assert headers["x-mlflow-experiment-name"] == "my-experiment" + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + +class TestMlflowLogPromptsHeader: + """Verify x-mlflow-log-prompts header when log_prompts is enabled.""" + + def test_log_prompts_header_when_true(self, monkeypatch): + """When mlflow_log_prompts=True, header is sent.""" + monkeypatch.setenv("OTEL_ENABLED", "false") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + monkeypatch.setenv("MLFLOW_LOG_PROMPTS", "true") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + + with patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ): + telemetry_mod.setup_telemetry() + + call_kwargs = mock_http_exporter_cls.call_args + headers = call_kwargs.kwargs.get("headers", {}) + assert headers["x-mlflow-log-prompts"] == "true" + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + def test_no_log_prompts_header_when_false(self, monkeypatch): + """When mlflow_log_prompts=False, no header is sent.""" + monkeypatch.setenv("OTEL_ENABLED", "false") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + monkeypatch.setenv("MLFLOW_LOG_PROMPTS", "false") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + + with patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ): + telemetry_mod.setup_telemetry() + + call_kwargs = mock_http_exporter_cls.call_args + headers = call_kwargs.kwargs.get("headers", {}) + assert "x-mlflow-log-prompts" not in headers + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + +class TestMlflowRunTagsHeader: + """Verify x-mlflow-run-tags header when run_tags is set.""" + + def test_run_tags_header_when_set(self, monkeypatch): + """When mlflow_run_tags is set, header is sent.""" + monkeypatch.setenv("OTEL_ENABLED", "false") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + monkeypatch.setenv("MLFLOW_RUN_TAGS", "env=prod,team=ai") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + + with patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ): + telemetry_mod.setup_telemetry() + + call_kwargs = mock_http_exporter_cls.call_args + headers = call_kwargs.kwargs.get("headers", {}) + assert headers["x-mlflow-run-tags"] == "env=prod,team=ai" + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + +class TestMlflowTrailingSlash: + """Verify trailing slash in tracking_uri is stripped.""" + + def test_trailing_slash_stripped(self, monkeypatch): + """Trailing slash in tracking URI doesn't produce double-slash.""" + monkeypatch.setenv("OTEL_ENABLED", "false") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000/") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + mock_http_exporter_cls = MagicMock() + + with patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter", + mock_http_exporter_cls, + ): + telemetry_mod.setup_telemetry() + + call_kwargs = mock_http_exporter_cls.call_args + endpoint = call_kwargs.kwargs.get( + "endpoint", call_kwargs.args[0] if call_kwargs.args else None + ) + assert endpoint == "http://mlflow.local:5000/v1/traces" + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() + + +class TestMlflowImportError: + """Verify ImportError is handled when exporter package is missing.""" + + def test_import_error_raises(self, monkeypatch): + """When opentelemetry-exporter-otlp-proto-http is missing, ImportError is raised.""" + monkeypatch.setenv("OTEL_ENABLED", "false") + monkeypatch.setenv("MLFLOW_ENABLED", "true") + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://mlflow.local:5000") + get_settings.cache_clear() + + import lightspeed_agent.telemetry.setup as telemetry_mod + + telemetry_mod._tracer_provider = None + + with ( + patch.dict( + "sys.modules", + {"opentelemetry.exporter.otlp.proto.http.trace_exporter": None}, + ), + pytest.raises(ImportError), + ): + telemetry_mod.setup_telemetry() + + telemetry_mod._tracer_provider = None + get_settings.cache_clear() From 2fd3943bae1199f920347121ca90dc729e4ca67a Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Thu, 25 Jun 2026 10:33:16 +0200 Subject: [PATCH 2/4] feat: add MLflow configuration for Cloud Run deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Cloud Run the agent connects to an external MLflow instance (Red Hat infrastructure) — no new GCP services deployed. - service.yaml: MLFLOW_ENABLED, MLFLOW_TRACKING_URI, MLFLOW_EXPERIMENT_NAME, MLFLOW_EXPERIMENT_ID, MLFLOW_LOG_PROMPTS, MLFLOW_RUN_TAGS env vars - README.md: MLflow Tracing section with configuration table, enabling instructions, networking/auth notes, and dependency info Co-Authored-By: Claude Opus 4.6 (1M context) --- deploy/cloudrun/README.md | 56 ++++++++++++++++++++++++++++++++++++ deploy/cloudrun/service.yaml | 13 +++++++++ 2 files changed, 69 insertions(+) diff --git a/deploy/cloudrun/README.md b/deploy/cloudrun/README.md index 9d610448..5afb21e8 100644 --- a/deploy/cloudrun/README.md +++ b/deploy/cloudrun/README.md @@ -2616,6 +2616,62 @@ gcloud logging read 'resource.type="cloud_run_revision" AND resource.labels.serv No additional configuration is required — audit logging is automatically active when `LOG_FORMAT=json`. +## MLflow Tracing (LLM Observability) + +The agent supports sending LLM traces to an external MLflow Tracking Server via +an OpenTelemetry bridge. Google ADK natively generates OTel traces for agent +runs, LLM calls, and tool invocations — MLflow accepts them via its OTLP +endpoint at `/v1/traces`. + +On Cloud Run, MLflow is **not deployed as a new service** — the agent connects +to an existing MLflow instance on Red Hat infrastructure (or any external MLflow +server). No additional Cloud SQL databases or GCS buckets are needed. + +### Configuration + +Set the following environment variables on the agent service (already present in +`service.yaml` with defaults): + +| Variable | Default | Description | +|----------|---------|-------------| +| `MLFLOW_ENABLED` | `false` | Enable MLflow tracing | +| `MLFLOW_TRACKING_URI` | (empty) | External MLflow server URL (e.g., `https://mlflow.example.redhat.com`) | +| `MLFLOW_EXPERIMENT_NAME` | `lightspeed-agent` | MLflow experiment name | +| `MLFLOW_LOG_PROMPTS` | `false` | Log prompts/responses (**security-sensitive**) | + +### Enabling MLflow + +```bash +gcloud run services update ${SERVICE_NAME:-lightspeed-agent} \ + --region=$GOOGLE_CLOUD_LOCATION \ + --project=$GOOGLE_CLOUD_PROJECT \ + --update-env-vars="\ +MLFLOW_ENABLED=true,\ +MLFLOW_TRACKING_URI=https://mlflow.example.redhat.com" +``` + +**Networking:** If the MLflow instance is on a public HTTPS endpoint, no +additional configuration is needed — Cloud Run has outbound internet access by +default. If it's behind a VPN or private network, configure a VPC connector + +Cloud VPN or Cloud Interconnect (the existing VPC connector for Redis can be +reused). + +**Authentication:** If the MLflow instance requires auth, set credentials via +Secret Manager and configure the `OTEL_EXPORTER_OTLP_HEADERS` env var with +the appropriate auth headers. + +### Dependencies + +The agent container needs the `mlflow-tracing` package (~5 MB lightweight SDK) +and `opentelemetry-exporter-otlp-proto-http`. Install via the `mlflow` optional +dependency group: + +```bash +pip install 'lightspeed-agent[mlflow]' +``` + +Or include in the Containerfile when building with MLflow support. + ## Monitoring ### Built-in Cloud Run Metrics diff --git a/deploy/cloudrun/service.yaml b/deploy/cloudrun/service.yaml index 6de8e79a..91405238 100644 --- a/deploy/cloudrun/service.yaml +++ b/deploy/cloudrun/service.yaml @@ -187,6 +187,19 @@ spec: value: "8002" - name: FORWARDED_ALLOW_IPS value: "*" + # MLflow Configuration + - name: MLFLOW_ENABLED + value: "${MLFLOW_ENABLED:-false}" + - name: MLFLOW_TRACKING_URI + value: "${MLFLOW_TRACKING_URI:-}" + - name: MLFLOW_EXPERIMENT_NAME + value: "${MLFLOW_EXPERIMENT_NAME:-lightspeed-agent}" + - name: MLFLOW_EXPERIMENT_ID + value: "${MLFLOW_EXPERIMENT_ID:-}" + - name: MLFLOW_LOG_PROMPTS + value: "${MLFLOW_LOG_PROMPTS:-false}" + - name: MLFLOW_RUN_TAGS + value: "${MLFLOW_RUN_TAGS:-}" volumeMounts: - name: redis-ca-cert mountPath: /secrets/redis-ca-cert From 420786bbe76801585fae3e8ddd305cdd2bb70b49 Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Thu, 25 Jun 2026 12:14:26 +0200 Subject: [PATCH 3/4] feat: add MLflow Helm templates for OpenShift deployment Deploy MLflow Tracking Server as an optional Helm component with dedicated PostgreSQL, artifact PVC, NetworkPolicy, and optional Route. New templates: - mlflow-deployment.yaml: MLflow server with security contexts, readiness probe (/api/2.0/mlflow/experiments/search), liveness probe (tcpSocket), artifact PVC + tmp emptyDir mounts - mlflow-service.yaml: ClusterIP service - mlflow-route.yaml: optional OpenShift Route - mlflow-pvc.yaml: artifact storage (10Gi default) - mlflow-postgresql-deployment.yaml: dedicated PostgreSQL + Service - mlflow-postgresql-pvc.yaml: PostgreSQL data (5Gi default) - networkpolicy-mlflow.yaml: restricts ingress to agent pods only Modified: - _helpers.tpl: MLflow + PostgreSQL service name and selector helpers - configmap.yaml: MLflow env vars with auto-resolved trackingUri pointing to the ClusterIP Service - secret.yaml: MLFLOW_DB_PASSWORD for dedicated PostgreSQL - values.yaml: mlflow section with pinned image (v3.14.0), resource defaults, storage, route, auth, and postgresql config - README.md: MLflow Tracking section with value table and usage docs Co-Authored-By: Claude Opus 4.6 (1M context) --- deploy/openshift/README.md | 61 ++++++++++ deploy/openshift/templates/_helpers.tpl | 30 +++++ deploy/openshift/templates/configmap.yaml | 14 +++ .../templates/mlflow-deployment.yaml | 91 ++++++++++++++ .../mlflow-postgresql-deployment.yaml | 112 ++++++++++++++++++ .../templates/mlflow-postgresql-pvc.yaml | 18 +++ deploy/openshift/templates/mlflow-pvc.yaml | 18 +++ deploy/openshift/templates/mlflow-route.yaml | 20 ++++ .../openshift/templates/mlflow-service.yaml | 18 +++ .../templates/networkpolicy-mlflow.yaml | 28 +++++ deploy/openshift/templates/secret.yaml | 3 + deploy/openshift/values.yaml | 43 +++++++ 12 files changed, 456 insertions(+) create mode 100644 deploy/openshift/templates/mlflow-deployment.yaml create mode 100644 deploy/openshift/templates/mlflow-postgresql-deployment.yaml create mode 100644 deploy/openshift/templates/mlflow-postgresql-pvc.yaml create mode 100644 deploy/openshift/templates/mlflow-pvc.yaml create mode 100644 deploy/openshift/templates/mlflow-route.yaml create mode 100644 deploy/openshift/templates/mlflow-service.yaml create mode 100644 deploy/openshift/templates/networkpolicy-mlflow.yaml diff --git a/deploy/openshift/README.md b/deploy/openshift/README.md index d9189980..9f649b13 100644 --- a/deploy/openshift/README.md +++ b/deploy/openshift/README.md @@ -77,6 +77,8 @@ a web interface to: | **postgresql** | PostgreSQL 16 for session persistence | Both (only when `sessionBackend: database`) | | **marketplace-postgresql** | PostgreSQL 16 for marketplace/entitlement data | Standalone only | | **redis** | Redis 7 for distributed rate limiting | Both modes | +| **mlflow** | MLflow Tracking Server for LLM observability | Both (only when `mlflow.enabled: true`) | +| **mlflow-postgresql** | PostgreSQL 16 for MLflow metadata | Both (only when `mlflow.enabled` and `mlflow.postgresql.mode: dedicated`) | | **marketplace-handler** | DCR and marketplace event handler | Standalone only | | **standalone-ui** | Web UI for DCR + A2A testing | Standalone only | @@ -1077,6 +1079,61 @@ they are distinguishable in PromQL queries. > `monitoring.serviceMonitor.*`, `monitoring.grafanaDashboard.*`). Only > `otel.serviceName` must be set per agent since it must be unique. +### MLflow Tracking (LLM Observability) + +MLflow provides LLM-specific tracing — token counts, latency, tool call traces, +and optional prompt/response logging. It works via an OpenTelemetry bridge: +Google ADK natively generates OTel traces, and MLflow accepts them via its OTLP +endpoint at `/v1/traces`. The agent adds a second OTel span processor alongside +the existing exporters. Requires MLflow >= 3.6.0 on the server side and the +lightweight `mlflow-tracing` package (~5 MB) on the agent side. + +| Value | Description | Default | +|---|---|---| +| `mlflow.enabled` | Deploy MLflow Tracking Server and enable agent tracing | `false` | +| `mlflow.trackingUri` | MLflow server URI (on OCP the ConfigMap auto-resolves to the ClusterIP Service) | `http://localhost:5000` | +| `mlflow.experimentName` | MLflow experiment name | `lightspeed-agent` | +| `mlflow.experimentId` | MLflow experiment ID (sent as `x-mlflow-experiment-id` OTLP header) | `""` | +| `mlflow.logPrompts` | Log LLM prompts/responses (**security-sensitive** — may contain PII) | `false` | +| `mlflow.runTags` | Extra run tags as `key=value` pairs | `""` | +| `mlflow.image.repository` | MLflow container image | `ghcr.io/mlflow/mlflow` | +| `mlflow.image.tag` | Image tag | `v3.14.0` | +| `mlflow.port` | MLflow server port | `5000` | +| `mlflow.route.enabled` | Create an OpenShift Route for the MLflow UI | `false` | +| `mlflow.auth.type` | Auth type: `none`, `oidc`, `proxy` | `none` | +| `mlflow.postgresql.mode` | `dedicated` (new PostgreSQL) or `shared` (reuse marketplace PostgreSQL) | `dedicated` | +| `mlflow.postgresql.database` | Database name | `mlflow_tracking` | +| `mlflow.storage.artifactSize` | PVC size for MLflow artifacts | `10Gi` | +| `secrets.mlflowDbPassword` | MLflow dedicated PostgreSQL password | `""` | + +**Enabling MLflow:** + +```yaml +# In my-values.yaml: +mlflow: + enabled: true + +# In secrets.yaml (when using dedicated PostgreSQL): +secrets: + mlflowDbPassword: "a-strong-mlflow-password" +``` + +After install, the agent automatically sends OTel traces to the MLflow server. +Access the MLflow UI via `oc port-forward`: + +```bash +oc port-forward svc/lightspeed-agent-mlflow 5000:5000 -n lightspeed-agent +# Open http://localhost:5000 in your browser +``` + +Or enable an OpenShift Route for persistent access: + +```yaml +mlflow: + route: + enabled: true +``` + ## Authentication The agent authenticates requests via Red Hat SSO token introspection: @@ -1194,6 +1251,10 @@ oc logs deployment/lightspeed-agent-postgresql -n lightspeed-agent oc logs deployment/lightspeed-agent-marketplace-postgresql -n lightspeed-agent # Redis oc logs deployment/lightspeed-agent-redis -n lightspeed-agent +# MLflow (only when mlflow.enabled=true) +oc logs deployment/lightspeed-agent-mlflow -n lightspeed-agent +# MLflow PostgreSQL (only when mlflow.enabled and mlflow.postgresql.mode=dedicated) +oc logs deployment/lightspeed-agent-mlflow-postgresql -n lightspeed-agent ``` ### Common issues diff --git a/deploy/openshift/templates/_helpers.tpl b/deploy/openshift/templates/_helpers.tpl index 6127a555..82c59e26 100644 --- a/deploy/openshift/templates/_helpers.tpl +++ b/deploy/openshift/templates/_helpers.tpl @@ -116,6 +116,36 @@ UI service name {{- include "lightspeed-agent.fullname" . }}-ui {{- end }} +{{/* +MLflow service name +*/}} +{{- define "lightspeed-agent.mlflowServiceName" -}} +{{- include "lightspeed-agent.fullname" . }}-mlflow +{{- end }} + +{{/* +Selector labels for MLflow +*/}} +{{- define "lightspeed-agent.mlflowSelectorLabels" -}} +app.kubernetes.io/name: {{ include "lightspeed-agent.fullname" . }}-mlflow +app.kubernetes.io/component: mlflow +{{- end }} + +{{/* +MLflow PostgreSQL service name +*/}} +{{- define "lightspeed-agent.mlflowPostgresqlServiceName" -}} +{{- include "lightspeed-agent.fullname" . }}-mlflow-postgresql +{{- end }} + +{{/* +Selector labels for MLflow PostgreSQL +*/}} +{{- define "lightspeed-agent.mlflowPostgresqlSelectorLabels" -}} +app.kubernetes.io/name: {{ include "lightspeed-agent.fullname" . }}-mlflow-postgresql +app.kubernetes.io/component: mlflow-database +{{- end }} + {{/* Whether the handler should be deployed on OCP. Only deployed in standalone mode — in hybrid the handler stays on GCP. diff --git a/deploy/openshift/templates/configmap.yaml b/deploy/openshift/templates/configmap.yaml index a613886f..ac02d5f1 100644 --- a/deploy/openshift/templates/configmap.yaml +++ b/deploy/openshift/templates/configmap.yaml @@ -131,3 +131,17 @@ data: OTEL_METRICS_ENABLED: {{ .Values.otel.metricsEnabled | quote }} OTEL_METRICS_PROMETHEUS_PORT: {{ .Values.otel.metricsPrometheusPort | quote }} OTEL_METRICS_COLLECTION_INTERVAL: {{ .Values.otel.metricsCollectionInterval | quote }} + + # MLflow + MLFLOW_ENABLED: {{ .Values.mlflow.enabled | quote }} + {{- if .Values.mlflow.enabled }} + MLFLOW_TRACKING_URI: "http://{{ include "lightspeed-agent.mlflowServiceName" . }}:{{ .Values.mlflow.port }}" + MLFLOW_EXPERIMENT_NAME: {{ .Values.mlflow.experimentName | quote }} + {{- if .Values.mlflow.experimentId }} + MLFLOW_EXPERIMENT_ID: {{ .Values.mlflow.experimentId | quote }} + {{- end }} + MLFLOW_LOG_PROMPTS: {{ .Values.mlflow.logPrompts | quote }} + {{- if .Values.mlflow.runTags }} + MLFLOW_RUN_TAGS: {{ .Values.mlflow.runTags | quote }} + {{- end }} + {{- end }} diff --git a/deploy/openshift/templates/mlflow-deployment.yaml b/deploy/openshift/templates/mlflow-deployment.yaml new file mode 100644 index 00000000..09360225 --- /dev/null +++ b/deploy/openshift/templates/mlflow-deployment.yaml @@ -0,0 +1,91 @@ +{{- if .Values.mlflow.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "lightspeed-agent.mlflowServiceName" . }} + labels: + {{- include "lightspeed-agent.labels" . | nindent 4 }} + app.kubernetes.io/component: mlflow +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "lightspeed-agent.mlflowSelectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "lightspeed-agent.mlflowSelectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + runAsNonRoot: true + containers: + - name: mlflow + image: {{ printf "%s:%s" .Values.mlflow.image.repository .Values.mlflow.image.tag }} + imagePullPolicy: {{ .Values.mlflow.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + args: + - "mlflow" + - "server" + - "--host" + - "0.0.0.0" + - "--port" + - {{ .Values.mlflow.port | quote }} + - "--artifacts-destination" + - "/mlflow/artifacts" + {{- if eq .Values.mlflow.postgresql.mode "dedicated" }} + - "--backend-store-uri" + - "postgresql://$(MLFLOW_DB_USER):$(MLFLOW_DB_PASSWORD)@$(MLFLOW_DB_HOST):5432/$(MLFLOW_DB_NAME)" + {{- end }} + ports: + - containerPort: {{ .Values.mlflow.port }} + protocol: TCP + env: + {{- if eq .Values.mlflow.postgresql.mode "dedicated" }} + - name: MLFLOW_DB_USER + value: {{ .Values.mlflow.postgresql.user | quote }} + - name: MLFLOW_DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "lightspeed-agent.fullname" . }}-secrets + key: MLFLOW_DB_PASSWORD + - name: MLFLOW_DB_HOST + value: {{ include "lightspeed-agent.mlflowPostgresqlServiceName" . }} + - name: MLFLOW_DB_NAME + value: {{ .Values.mlflow.postgresql.database | quote }} + {{- end }} + volumeMounts: + - name: mlflow-artifacts + mountPath: /mlflow/artifacts + - name: tmp + mountPath: /tmp + resources: + {{- toYaml .Values.mlflow.resources | nindent 12 }} + readinessProbe: + httpGet: + path: /api/2.0/mlflow/experiments/search?max_results=1 + port: {{ .Values.mlflow.port }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: {{ .Values.mlflow.port }} + initialDelaySeconds: 30 + periodSeconds: 10 + volumes: + - name: mlflow-artifacts + persistentVolumeClaim: + claimName: {{ include "lightspeed-agent.fullname" . }}-mlflow-artifacts + - name: tmp + emptyDir: {} +{{- end }} diff --git a/deploy/openshift/templates/mlflow-postgresql-deployment.yaml b/deploy/openshift/templates/mlflow-postgresql-deployment.yaml new file mode 100644 index 00000000..49fdf8c2 --- /dev/null +++ b/deploy/openshift/templates/mlflow-postgresql-deployment.yaml @@ -0,0 +1,112 @@ +{{- if and .Values.mlflow.enabled (eq .Values.mlflow.postgresql.mode "dedicated") }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "lightspeed-agent.mlflowPostgresqlServiceName" . }} + labels: + {{- include "lightspeed-agent.labels" . | nindent 4 }} + app.kubernetes.io/component: mlflow-database +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "lightspeed-agent.mlflowPostgresqlSelectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "lightspeed-agent.mlflowPostgresqlSelectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + runAsNonRoot: true + containers: + - name: postgresql + image: {{ printf "%s:%s" .Values.postgresql.image.repository .Values.postgresql.image.tag }} + imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + ports: + - containerPort: 5432 + protocol: TCP + env: + - name: POSTGRESQL_USER + value: {{ .Values.mlflow.postgresql.user | quote }} + - name: POSTGRESQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "lightspeed-agent.fullname" . }}-secrets + key: MLFLOW_DB_PASSWORD + - name: POSTGRESQL_DATABASE + value: {{ .Values.mlflow.postgresql.database | quote }} + volumeMounts: + - name: postgresql-home + mountPath: /var/lib/pgsql + - name: postgresql-data + mountPath: /var/lib/pgsql/data + - name: tmp + mountPath: /tmp + - name: run-postgresql + mountPath: /var/run/postgresql + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + startupProbe: + exec: + command: + - /usr/libexec/check-container + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 15 + readinessProbe: + exec: + command: + - /usr/libexec/check-container + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + exec: + command: + - /usr/libexec/check-container + - --live + initialDelaySeconds: 30 + periodSeconds: 10 + volumes: + - name: postgresql-data + persistentVolumeClaim: + claimName: {{ include "lightspeed-agent.fullname" . }}-mlflow-postgresql-data + - name: postgresql-home + emptyDir: {} + - name: tmp + emptyDir: {} + - name: run-postgresql + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "lightspeed-agent.mlflowPostgresqlServiceName" . }} + labels: + {{- include "lightspeed-agent.labels" . | nindent 4 }} + app.kubernetes.io/component: mlflow-database +spec: + selector: + {{- include "lightspeed-agent.mlflowPostgresqlSelectorLabels" . | nindent 4 }} + ports: + - port: 5432 + targetPort: 5432 + protocol: TCP + type: ClusterIP +{{- end }} diff --git a/deploy/openshift/templates/mlflow-postgresql-pvc.yaml b/deploy/openshift/templates/mlflow-postgresql-pvc.yaml new file mode 100644 index 00000000..9cc385de --- /dev/null +++ b/deploy/openshift/templates/mlflow-postgresql-pvc.yaml @@ -0,0 +1,18 @@ +{{- if and .Values.mlflow.enabled (eq .Values.mlflow.postgresql.mode "dedicated") }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "lightspeed-agent.fullname" . }}-mlflow-postgresql-data + labels: + {{- include "lightspeed-agent.labels" . | nindent 4 }} + app.kubernetes.io/component: mlflow-database +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.mlflow.postgresql.storage.size }} + {{- if .Values.mlflow.postgresql.storage.storageClassName }} + storageClassName: {{ .Values.mlflow.postgresql.storage.storageClassName | quote }} + {{- end }} +{{- end }} diff --git a/deploy/openshift/templates/mlflow-pvc.yaml b/deploy/openshift/templates/mlflow-pvc.yaml new file mode 100644 index 00000000..252d9c20 --- /dev/null +++ b/deploy/openshift/templates/mlflow-pvc.yaml @@ -0,0 +1,18 @@ +{{- if .Values.mlflow.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "lightspeed-agent.fullname" . }}-mlflow-artifacts + labels: + {{- include "lightspeed-agent.labels" . | nindent 4 }} + app.kubernetes.io/component: mlflow +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.mlflow.storage.artifactSize }} + {{- if .Values.mlflow.storage.storageClassName }} + storageClassName: {{ .Values.mlflow.storage.storageClassName | quote }} + {{- end }} +{{- end }} diff --git a/deploy/openshift/templates/mlflow-route.yaml b/deploy/openshift/templates/mlflow-route.yaml new file mode 100644 index 00000000..ab480288 --- /dev/null +++ b/deploy/openshift/templates/mlflow-route.yaml @@ -0,0 +1,20 @@ +{{- if and .Values.mlflow.enabled .Values.mlflow.route.enabled }} +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: {{ include "lightspeed-agent.mlflowServiceName" . }} + labels: + {{- include "lightspeed-agent.labels" . | nindent 4 }} + app.kubernetes.io/component: mlflow +spec: + to: + kind: Service + name: {{ include "lightspeed-agent.mlflowServiceName" . }} + weight: 100 + port: + targetPort: http + tls: + termination: {{ .Values.route.tls.termination }} + insecureEdgeTerminationPolicy: {{ .Values.route.tls.insecureEdgeTerminationPolicy }} + wildcardPolicy: None +{{- end }} diff --git a/deploy/openshift/templates/mlflow-service.yaml b/deploy/openshift/templates/mlflow-service.yaml new file mode 100644 index 00000000..c103902a --- /dev/null +++ b/deploy/openshift/templates/mlflow-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.mlflow.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "lightspeed-agent.mlflowServiceName" . }} + labels: + {{- include "lightspeed-agent.labels" . | nindent 4 }} + app.kubernetes.io/component: mlflow +spec: + selector: + {{- include "lightspeed-agent.mlflowSelectorLabels" . | nindent 4 }} + ports: + - port: {{ .Values.mlflow.port }} + targetPort: {{ .Values.mlflow.port }} + protocol: TCP + name: http + type: ClusterIP +{{- end }} diff --git a/deploy/openshift/templates/networkpolicy-mlflow.yaml b/deploy/openshift/templates/networkpolicy-mlflow.yaml new file mode 100644 index 00000000..628a8fe6 --- /dev/null +++ b/deploy/openshift/templates/networkpolicy-mlflow.yaml @@ -0,0 +1,28 @@ +{{- if .Values.mlflow.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "lightspeed-agent.mlflowServiceName" . }} + labels: + {{- include "lightspeed-agent.labels" . | nindent 4 }} + app.kubernetes.io/component: mlflow +spec: + podSelector: + matchLabels: + {{- include "lightspeed-agent.mlflowSelectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + {{- include "lightspeed-agent.agentSelectorLabels" . | nindent 14 }} + {{- if .Values.mlflow.route.enabled }} + - namespaceSelector: + matchLabels: + network.openshift.io/policy-group: ingress + {{- end }} + ports: + - protocol: TCP + port: {{ .Values.mlflow.port }} +{{- end }} diff --git a/deploy/openshift/templates/secret.yaml b/deploy/openshift/templates/secret.yaml index bc5722a4..24cdb512 100644 --- a/deploy/openshift/templates/secret.yaml +++ b/deploy/openshift/templates/secret.yaml @@ -28,6 +28,9 @@ stringData: GMA_CLIENT_SECRET: {{ .Values.secrets.gmaClientSecret | quote }} REDIS_PASSWORD: {{ .Values.secrets.redisPassword | quote }} LLM_API_KEY: {{ .Values.secrets.llmApiKey | quote }} + {{- if and .Values.mlflow.enabled (eq .Values.mlflow.postgresql.mode "dedicated") }} + MLFLOW_DB_PASSWORD: {{ .Values.secrets.mlflowDbPassword | quote }} + {{- end }} REDIS_CONF: | requirepass {{ .Values.secrets.redisPassword }} {{- end }} diff --git a/deploy/openshift/values.yaml b/deploy/openshift/values.yaml index e8eb2e25..13036a52 100644 --- a/deploy/openshift/values.yaml +++ b/deploy/openshift/values.yaml @@ -208,6 +208,47 @@ monitoring: # Namespace for the GrafanaDashboard CR — empty = release namespace namespace: "" +# --------------------------------------------------------------------------- +# MLflow Tracking Server (optional) +# --------------------------------------------------------------------------- +mlflow: + enabled: false + trackingUri: "http://localhost:5000" + experimentName: lightspeed-agent + experimentId: "" + logPrompts: false + runTags: "" + image: + repository: ghcr.io/mlflow/mlflow + tag: "v3.14.0" + pullPolicy: IfNotPresent + port: 5000 + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: "2" + memory: 4Gi + storage: + artifactSize: 10Gi + # storageClassName: "" # uncomment to use a specific storage class + route: + enabled: false + auth: + type: none + oidc: + issuer: "" + clientId: "" + clientSecret: "" + postgresql: + mode: dedicated + user: mlflow + database: mlflow_tracking + storage: + size: 5Gi + # storageClassName: "" # uncomment to use a specific storage class + # --------------------------------------------------------------------------- # Session PostgreSQL database # --------------------------------------------------------------------------- @@ -394,3 +435,5 @@ secrets: gcpServiceAccountKey: "" # API key for non-Google LLM providers (litellm only) llmApiKey: "" + # MLflow dedicated PostgreSQL password (only needed when mlflow.enabled and mlflow.postgresql.mode is "dedicated") + mlflowDbPassword: "" From 7ec5141ed0157a6eccdf24422f815823b66aaaad Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Fri, 26 Jun 2026 12:36:45 +0200 Subject: [PATCH 4/4] fix: enforce PostgreSQL backend for MLflow and correct docs - Add Helm fail guard when mlflow.postgresql.mode != 'dedicated' to prevent silent OTLP trace ingestion failure (file-based backends silently drop traces per MLflow docs) - Replace incorrect mlflow-tracing package references in Cloud Run and OpenShift READMEs with the actual dependency (opentelemetry-exporter-otlp-proto-http, already in the container) - Document that MLFLOW_EXPERIMENT_ID requires the experiment to already exist on the server (use MLFLOW_EXPERIMENT_NAME for auto-creation) Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 4 +++- deploy/cloudrun/README.md | 12 +++++++----- deploy/openshift/README.md | 7 ++++--- deploy/openshift/templates/mlflow-deployment.yaml | 4 +++- deploy/openshift/values.yaml | 3 +++ src/lightspeed_agent/config/settings.py | 6 +++++- src/lightspeed_agent/telemetry/setup.py | 6 +++++- 7 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 949ad31d..87ea8b49 100644 --- a/.env.example +++ b/.env.example @@ -297,7 +297,9 @@ OTEL_TRACES_SAMPLER_ARG=1.0 # MLflow experiment name # MLFLOW_EXPERIMENT_NAME=lightspeed-agent -# MLflow experiment ID (sent as x-mlflow-experiment-id header; overrides name if set) +# MLflow experiment ID (sent as x-mlflow-experiment-id header; overrides name if set). +# The experiment must already exist on the MLflow server — use MLFLOW_EXPERIMENT_NAME +# instead for auto-creation on first trace. # MLFLOW_EXPERIMENT_ID= # Enable logging of prompts and responses in MLflow traces diff --git a/deploy/cloudrun/README.md b/deploy/cloudrun/README.md index 5afb21e8..b18d385a 100644 --- a/deploy/cloudrun/README.md +++ b/deploy/cloudrun/README.md @@ -2662,16 +2662,18 @@ the appropriate auth headers. ### Dependencies -The agent container needs the `mlflow-tracing` package (~5 MB lightweight SDK) -and `opentelemetry-exporter-otlp-proto-http`. Install via the `mlflow` optional -dependency group: +The agent needs `opentelemetry-exporter-otlp-proto-http` for OTLP span export to +MLflow. This package is already included in the default container image (pulled in +by `opentelemetry-exporter-otlp` in the `[agent]` extra) — no additional +installation is needed. + +For minimal (non-agent) installs, the `[mlflow]` optional dependency group +provides only the OTLP HTTP exporter: ```bash pip install 'lightspeed-agent[mlflow]' ``` -Or include in the Containerfile when building with MLflow support. - ## Monitoring ### Built-in Cloud Run Metrics diff --git a/deploy/openshift/README.md b/deploy/openshift/README.md index 9f649b13..d561aaae 100644 --- a/deploy/openshift/README.md +++ b/deploy/openshift/README.md @@ -1085,15 +1085,16 @@ MLflow provides LLM-specific tracing — token counts, latency, tool call traces and optional prompt/response logging. It works via an OpenTelemetry bridge: Google ADK natively generates OTel traces, and MLflow accepts them via its OTLP endpoint at `/v1/traces`. The agent adds a second OTel span processor alongside -the existing exporters. Requires MLflow >= 3.6.0 on the server side and the -lightweight `mlflow-tracing` package (~5 MB) on the agent side. +the existing exporters. Requires MLflow >= 3.6.0 on the server side and +`opentelemetry-exporter-otlp-proto-http` on the agent side (already included in +the default container image via the `[agent]` dependency group). | Value | Description | Default | |---|---|---| | `mlflow.enabled` | Deploy MLflow Tracking Server and enable agent tracing | `false` | | `mlflow.trackingUri` | MLflow server URI (on OCP the ConfigMap auto-resolves to the ClusterIP Service) | `http://localhost:5000` | | `mlflow.experimentName` | MLflow experiment name | `lightspeed-agent` | -| `mlflow.experimentId` | MLflow experiment ID (sent as `x-mlflow-experiment-id` OTLP header) | `""` | +| `mlflow.experimentId` | MLflow experiment ID (sent as `x-mlflow-experiment-id` OTLP header). Must already exist on the server — use `experimentName` for auto-creation. | `""` | | `mlflow.logPrompts` | Log LLM prompts/responses (**security-sensitive** — may contain PII) | `false` | | `mlflow.runTags` | Extra run tags as `key=value` pairs | `""` | | `mlflow.image.repository` | MLflow container image | `ghcr.io/mlflow/mlflow` | diff --git a/deploy/openshift/templates/mlflow-deployment.yaml b/deploy/openshift/templates/mlflow-deployment.yaml index 09360225..c71be1f6 100644 --- a/deploy/openshift/templates/mlflow-deployment.yaml +++ b/deploy/openshift/templates/mlflow-deployment.yaml @@ -43,9 +43,11 @@ spec: - {{ .Values.mlflow.port | quote }} - "--artifacts-destination" - "/mlflow/artifacts" - {{- if eq .Values.mlflow.postgresql.mode "dedicated" }} - "--backend-store-uri" + {{- if eq .Values.mlflow.postgresql.mode "dedicated" }} - "postgresql://$(MLFLOW_DB_USER):$(MLFLOW_DB_PASSWORD)@$(MLFLOW_DB_HOST):5432/$(MLFLOW_DB_NAME)" + {{- else }} + {{- fail "mlflow.postgresql.mode must be 'dedicated' — MLflow requires a SQL backend for OpenTelemetry OTLP trace ingestion (file-based backends silently drop traces)" }} {{- end }} ports: - containerPort: {{ .Values.mlflow.port }} diff --git a/deploy/openshift/values.yaml b/deploy/openshift/values.yaml index 13036a52..b6a780fe 100644 --- a/deploy/openshift/values.yaml +++ b/deploy/openshift/values.yaml @@ -215,6 +215,7 @@ mlflow: enabled: false trackingUri: "http://localhost:5000" experimentName: lightspeed-agent + # Must already exist on the MLflow server — use experimentName for auto-creation. experimentId: "" logPrompts: false runTags: "" @@ -242,6 +243,8 @@ mlflow: clientId: "" clientSecret: "" postgresql: + # Must be "dedicated" — MLflow requires a SQL backend for OTLP trace ingestion. + # File-based backends silently drop traces. The chart fails if set otherwise. mode: dedicated user: mlflow database: mlflow_tracking diff --git a/src/lightspeed_agent/config/settings.py b/src/lightspeed_agent/config/settings.py index 53be9585..2f79ddfe 100644 --- a/src/lightspeed_agent/config/settings.py +++ b/src/lightspeed_agent/config/settings.py @@ -671,7 +671,11 @@ def _validate_session_backend(self) -> "Settings": ) mlflow_experiment_id: str = Field( default="", - description="MLflow experiment ID (sent as x-mlflow-experiment-id header)", + description=( + "MLflow experiment ID (sent as x-mlflow-experiment-id header). " + "The experiment must already exist on the MLflow server — " + "use MLFLOW_EXPERIMENT_NAME instead for auto-creation on first trace." + ), ) mlflow_log_prompts: bool = Field( default=False, diff --git a/src/lightspeed_agent/telemetry/setup.py b/src/lightspeed_agent/telemetry/setup.py index 84e2c9e5..5a95aa87 100644 --- a/src/lightspeed_agent/telemetry/setup.py +++ b/src/lightspeed_agent/telemetry/setup.py @@ -168,7 +168,11 @@ def setup_telemetry() -> None: settings = get_settings() - if not settings.otel_enabled and not settings.otel_metrics_enabled and not settings.mlflow_enabled: + if ( + not settings.otel_enabled + and not settings.otel_metrics_enabled + and not settings.mlflow_enabled + ): logger.debug("OpenTelemetry tracing, metrics, and MLflow are disabled") return