From 8a7b91980bdd5921d46a45dbee091f1973099eda Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 12 Sep 2025 10:51:43 +0200 Subject: [PATCH 01/77] added sum to trend data --- app/services/health/clickhouse.py | 3 ++- app/services/health/duckdb_queries.py | 7 ++++--- app/services/health/elasticsearch.py | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/services/health/clickhouse.py b/app/services/health/clickhouse.py index d80d87b..b3803a1 100644 --- a/app/services/health/clickhouse.py +++ b/app/services/health/clickhouse.py @@ -33,7 +33,8 @@ def get_trend_data_from_ch( ) -> dict[str, Any]: return ch.inquire(f""" SELECT toStartOfInterval(startDate, INTERVAL 1 {interval}) AS interval, - AVG(value), MIN(value), MAX(value), COUNT(*) FROM {ch.db_name}.{ch.table_name} + AVG(value) AS average, SUM(value) AS sum, MIN(value) AS min, + MAX(value) AS max, COUNT(*) AS count FROM {ch.db_name}.{ch.table_name} WHERE type = '{record_type}' {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 6b90fae..7047106 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -45,11 +45,12 @@ def get_trend_data_from_duckdb( ) -> list[dict[str, Any]]: result = duckdb.sql(f""" SELECT time_bucket(INTERVAL '1 {interval}', startDate) AS interval, - AVG(value) AS average, MIN(value) AS min, MAX(value) AS max, COUNT(*) AS count + AVG(value) AS average, SUM(value) AS sum, + MIN(value) AS min, MAX(value) AS max, COUNT(*) AS count FROM read_parquet('{client.parquetpath}') WHERE type = '{record_type}' - {f"AND startDate >= '{date_from}'" if date_from else ""} - {f"AND startDate <= '{date_to}'" if date_to else ""} + {f"AND startDate >= '{date_from}'" if date_from else ""} + {f"AND startDate <= '{date_to}'" if date_to else ""} GROUP BY interval ORDER BY interval ASC """) return client.format_response(result) diff --git a/app/services/health/elasticsearch.py b/app/services/health/elasticsearch.py index 9f0d76a..dca5fb1 100644 --- a/app/services/health/elasticsearch.py +++ b/app/services/health/elasticsearch.py @@ -106,6 +106,7 @@ def get_trend_data_logic( "avg_value": {"avg": {"field": "value"}}, "min_value": {"min": {"field": "value"}}, "max_value": {"max": {"field": "value"}}, + "value_sum": {"sum": {"field": "value"}}, "count": {"value_count": {"field": "value"}}, }, }, @@ -121,6 +122,7 @@ def get_trend_data_logic( "avg_value": bucket["avg_value"]["value"], "min_value": bucket["min_value"]["value"], "max_value": bucket["max_value"]["value"], + "value_sum": bucket["value_sum"]["value"], "count": bucket["count"]["value"], }, ) From 7795bf7008dc7a11d4ee2e7b0160c6a7293cfe9e Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 12 Sep 2025 11:16:14 +0200 Subject: [PATCH 02/77] added device grouping to duckdb for test --- app/services/health/duckdb_queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 7047106..9714628 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -51,7 +51,7 @@ def get_trend_data_from_duckdb( WHERE type = '{record_type}' {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} - GROUP BY interval ORDER BY interval ASC + GROUP BY interval, device ORDER BY interval ASC """) return client.format_response(result) From a70831cad29a2ef0b1341b5521502b10bda60ebb Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 12 Sep 2025 11:23:50 +0200 Subject: [PATCH 03/77] added device as well to query --- app/services/health/clickhouse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/health/clickhouse.py b/app/services/health/clickhouse.py index b3803a1..e7b3497 100644 --- a/app/services/health/clickhouse.py +++ b/app/services/health/clickhouse.py @@ -32,7 +32,7 @@ def get_trend_data_from_ch( date_to: str | None = None, ) -> dict[str, Any]: return ch.inquire(f""" - SELECT toStartOfInterval(startDate, INTERVAL 1 {interval}) AS interval, + SELECT device, toStartOfInterval(startDate, INTERVAL 1 {interval}) AS interval, AVG(value) AS average, SUM(value) AS sum, MIN(value) AS min, MAX(value) AS max, COUNT(*) AS count FROM {ch.db_name}.{ch.table_name} WHERE type = '{record_type}' From 74c5428cc181f3a077904f80dc7472619a8b95df Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 12 Sep 2025 11:33:09 +0200 Subject: [PATCH 04/77] ch and duck device/interval grouping --- app/services/health/clickhouse.py | 5 ++++- app/services/health/duckdb_queries.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/services/health/clickhouse.py b/app/services/health/clickhouse.py index e7b3497..7cf09f5 100644 --- a/app/services/health/clickhouse.py +++ b/app/services/health/clickhouse.py @@ -38,7 +38,7 @@ def get_trend_data_from_ch( WHERE type = '{record_type}' {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} - GROUP BY interval ORDER BY interval ASC + GROUP BY interval, device ORDER BY interval ASC """) @@ -54,3 +54,6 @@ def search_values_from_ch( {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} """) + +if __name__ == "__main__": + print(get_trend_data_from_ch("HKQuantityTypeIdentifierStepCount", "week", "2023-03-01", "2023-04-01")) \ No newline at end of file diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 9714628..d4b0156 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -44,7 +44,7 @@ def get_trend_data_from_duckdb( date_to: str | None = None, ) -> list[dict[str, Any]]: result = duckdb.sql(f""" - SELECT time_bucket(INTERVAL '1 {interval}', startDate) AS interval, + SELECT device, time_bucket(INTERVAL '1 {interval}', startDate) AS interval, AVG(value) AS average, SUM(value) AS sum, MIN(value) AS min, MAX(value) AS max, COUNT(*) AS count FROM read_parquet('{client.parquetpath}') @@ -69,3 +69,6 @@ def search_values_from_duckdb( {f"AND startDate <= '{date_to}'" if date_to else ""} """) return client.format_response(result) + +if __name__ == "__main__": + print(get_trend_data_from_duckdb("HKQuantityTypeIdentifierStepCount", "week", "2023-03-01", "2023-04-01")) \ No newline at end of file From 954654c2adca352fff2b3526db59b7164b43dfe5 Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 12 Sep 2025 11:53:32 +0200 Subject: [PATCH 05/77] docstring tweak --- app/mcp/v1/tools/ch_reader.py | 5 +++++ app/mcp/v1/tools/duckdb_reader.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/app/mcp/v1/tools/ch_reader.py b/app/mcp/v1/tools/ch_reader.py index 6ccf2de..09788a8 100644 --- a/app/mcp/v1/tools/ch_reader.py +++ b/app/mcp/v1/tools/ch_reader.py @@ -142,9 +142,11 @@ def get_trend_data_ch( Returns: - record_type: The analyzed record type + - device: The device on which the data was recorded - interval: The time interval used - trend_data: List of time buckets with statistics for each period: * date: The time period (ISO string) + * value_sum: Sum of values for the period * avg_value: Average value for the period * min_value: Minimum value for the period * max_value: Maximum value for the period @@ -152,6 +154,9 @@ def get_trend_data_ch( Notes for LLMs: - Use this to analyze trends, patterns, and seasonal variations in health data + - Keep in mind that when there is data from multiple devices spanning the same + time period, there is a possibility of data being duplicated. Inform the user + of this possibility if you see multiple devices in the same time period. - The function automatically handles date filtering if date_from/date_to are provided - IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values. diff --git a/app/mcp/v1/tools/duckdb_reader.py b/app/mcp/v1/tools/duckdb_reader.py index f142ea9..e34bb5b 100644 --- a/app/mcp/v1/tools/duckdb_reader.py +++ b/app/mcp/v1/tools/duckdb_reader.py @@ -142,9 +142,11 @@ def get_trend_data_duckdb( Returns: - record_type: The analyzed record type + - device: The device on which the data was recorded - interval: The time interval used - trend_data: List of time buckets with statistics for each period: * date: The time period (ISO string) + * value_sum: Sum of values for the period * avg_value: Average value for the period * min_value: Minimum value for the period * max_value: Maximum value for the period @@ -152,6 +154,9 @@ def get_trend_data_duckdb( Notes for LLMs: - Use this to analyze trends, patterns, and seasonal variations in health data + - Keep in mind that when there is data from multiple devices spanning the same + time period, there is a possibility of data being duplicated. Inform the user + of this possibility if you see multiple devices in the same time period. - The function automatically handles date filtering if date_from/date_to are provided - IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values. From 1afd7fea514020d2aea9bc9cf80572e5a512414d Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 12 Sep 2025 14:58:55 +0200 Subject: [PATCH 06/77] docstring improving --- app/mcp/v1/tools/ch_reader.py | 2 ++ app/mcp/v1/tools/duckdb_reader.py | 2 ++ app/mcp/v1/tools/es_reader.py | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/app/mcp/v1/tools/ch_reader.py b/app/mcp/v1/tools/ch_reader.py index 09788a8..71b6a3d 100644 --- a/app/mcp/v1/tools/ch_reader.py +++ b/app/mcp/v1/tools/ch_reader.py @@ -157,6 +157,8 @@ def get_trend_data_ch( - Keep in mind that when there is data from multiple devices spanning the same time period, there is a possibility of data being duplicated. Inform the user of this possibility if you see multiple devices in the same time period. + - If a user asks you to sum up some values from their health records, DO NOT + search for records and write a script to sum them, instead, use this tool. - The function automatically handles date filtering if date_from/date_to are provided - IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values. diff --git a/app/mcp/v1/tools/duckdb_reader.py b/app/mcp/v1/tools/duckdb_reader.py index e34bb5b..7e367c3 100644 --- a/app/mcp/v1/tools/duckdb_reader.py +++ b/app/mcp/v1/tools/duckdb_reader.py @@ -157,6 +157,8 @@ def get_trend_data_duckdb( - Keep in mind that when there is data from multiple devices spanning the same time period, there is a possibility of data being duplicated. Inform the user of this possibility if you see multiple devices in the same time period. + - If a user asks you to sum up some values from their health records, DO NOT + search for records and write a script to sum them, instead, use this tool. - The function automatically handles date filtering if date_from/date_to are provided - IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values. diff --git a/app/mcp/v1/tools/es_reader.py b/app/mcp/v1/tools/es_reader.py index 2281819..a092bd2 100644 --- a/app/mcp/v1/tools/es_reader.py +++ b/app/mcp/v1/tools/es_reader.py @@ -142,9 +142,11 @@ def get_trend_data_es( Returns: - record_type: The analyzed record type + - device: The device on which the data was recorded - interval: The time interval used - trend_data: List of time buckets with statistics for each period: * date: The time period (ISO string) + * value_sum: Sum of values for the period * avg_value: Average value for the period * min_value: Minimum value for the period * max_value: Maximum value for the period @@ -152,6 +154,11 @@ def get_trend_data_es( Notes for LLMs: - Use this to analyze trends, patterns, and seasonal variations in health data + - Keep in mind that when there is data from multiple devices spanning the same + time period, there is a possibility of data being duplicated. Inform the user + of this possibility if you see multiple devices in the same time period. + - If a user asks you to sum up some values from their health records, DO NOT + search for records and write a script to sum them, instead, use this tool. - The function automatically handles date filtering if date_from/date_to are provided - IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values. From 0e42919cb32879e3b53999896fb2d1bd0135b4c0 Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 12 Sep 2025 15:34:01 +0200 Subject: [PATCH 07/77] remove debug code --- app/services/health/clickhouse.py | 3 --- app/services/health/duckdb_queries.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/app/services/health/clickhouse.py b/app/services/health/clickhouse.py index 7cf09f5..0a5bcd5 100644 --- a/app/services/health/clickhouse.py +++ b/app/services/health/clickhouse.py @@ -54,6 +54,3 @@ def search_values_from_ch( {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} """) - -if __name__ == "__main__": - print(get_trend_data_from_ch("HKQuantityTypeIdentifierStepCount", "week", "2023-03-01", "2023-04-01")) \ No newline at end of file diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index d4b0156..72b8bcf 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -69,6 +69,3 @@ def search_values_from_duckdb( {f"AND startDate <= '{date_to}'" if date_to else ""} """) return client.format_response(result) - -if __name__ == "__main__": - print(get_trend_data_from_duckdb("HKQuantityTypeIdentifierStepCount", "week", "2023-03-01", "2023-04-01")) \ No newline at end of file From c3cbcb6e1641b23838608142086926d73b6c9c88 Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 15 Sep 2025 09:26:55 +0200 Subject: [PATCH 08/77] standardise errors and change trend docstrings --- app/mcp/v1/tools/ch_reader.py | 9 ++++++--- app/mcp/v1/tools/duckdb_reader.py | 11 +++++++---- app/mcp/v1/tools/es_reader.py | 9 ++++++--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/mcp/v1/tools/ch_reader.py b/app/mcp/v1/tools/ch_reader.py index 71b6a3d..9043fc7 100644 --- a/app/mcp/v1/tools/ch_reader.py +++ b/app/mcp/v1/tools/ch_reader.py @@ -36,7 +36,7 @@ def get_health_summary_ch() -> dict[str, Any]: try: return get_health_summary_from_ch() except Exception as e: - return {"error": str(e)} + return {"error": f"Failed to get health summary: {str(e)}"} @ch_reader_router.tool @@ -71,7 +71,7 @@ def search_health_records_ch(params: HealthRecordSearchParams) -> dict[str, Any] try: return search_health_records_from_ch(params) except Exception as e: - return {"error": str(e)} + return {"error": f"Failed to search health records: {str(e)}"} @ch_reader_router.tool @@ -158,7 +158,10 @@ def get_trend_data_ch( time period, there is a possibility of data being duplicated. Inform the user of this possibility if you see multiple devices in the same time period. - If a user asks you to sum up some values from their health records, DO NOT - search for records and write a script to sum them, instead, use this tool. + search for records and write a script to sum them, instead, use this tool: + if they ask to sum data from a year, use this tool with date_from set as the + beginning of the year and date_to as the end of the year, with an interval + of 'year' - The function automatically handles date filtering if date_from/date_to are provided - IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values. diff --git a/app/mcp/v1/tools/duckdb_reader.py b/app/mcp/v1/tools/duckdb_reader.py index 7e367c3..b064cd1 100644 --- a/app/mcp/v1/tools/duckdb_reader.py +++ b/app/mcp/v1/tools/duckdb_reader.py @@ -36,7 +36,7 @@ def get_health_summary_duckdb() -> list[dict[str, Any]]: try: return get_health_summary_from_duckdb() except Exception as e: - return [{"error": str(e)}] + return [{"error": f"Failed to get health summary: {str(e)}"}] @duckdb_reader_router.tool @@ -71,7 +71,7 @@ def search_health_records_duckdb(params: HealthRecordSearchParams) -> list[dict[ try: return search_health_records_from_duckdb(params) except Exception as e: - return [{"error": str(e)}] + return [{"error": f"Failed to search health records: {str(e)}"}] @duckdb_reader_router.tool @@ -158,7 +158,10 @@ def get_trend_data_duckdb( time period, there is a possibility of data being duplicated. Inform the user of this possibility if you see multiple devices in the same time period. - If a user asks you to sum up some values from their health records, DO NOT - search for records and write a script to sum them, instead, use this tool. + search for records and write a script to sum them, instead, use this tool: + if they ask to sum data from a year, use this tool with date_from set as the + beginning of the year and date_to as the end of the year, with an interval + of 'year' - The function automatically handles date filtering if date_from/date_to are provided - IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values. @@ -214,4 +217,4 @@ def search_values_duckdb( try: return search_values_from_duckdb(record_type, value, date_from, date_to) except Exception as e: - return [{"error": f"Failed to get trend data: {str(e)}"}] + return [{"error": f"Failed to search for values: {str(e)}"}] diff --git a/app/mcp/v1/tools/es_reader.py b/app/mcp/v1/tools/es_reader.py index a092bd2..6cbcccd 100644 --- a/app/mcp/v1/tools/es_reader.py +++ b/app/mcp/v1/tools/es_reader.py @@ -36,7 +36,7 @@ def get_health_summary_es() -> dict[str, Any]: try: return get_health_summary_from_es() except Exception as e: - return {"error": f"Failed to get health summary from ES: {str(e)}"} + return {"error": f"Failed to get health summary: {str(e)}"} @es_reader_router.tool @@ -158,7 +158,10 @@ def get_trend_data_es( time period, there is a possibility of data being duplicated. Inform the user of this possibility if you see multiple devices in the same time period. - If a user asks you to sum up some values from their health records, DO NOT - search for records and write a script to sum them, instead, use this tool. + search for records and write a script to sum them, instead, use this tool: + if they ask to sum data from a year, use this tool with date_from set as the + beginning of the year and date_to as the end of the year, with an interval + of 'year' - The function automatically handles date filtering if date_from/date_to are provided - IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values. @@ -214,4 +217,4 @@ def search_values_es( try: return search_values_logic(record_type, value, date_from, date_to) except Exception as e: - return [{"error": f"Failed to get trend data: {str(e)}"}] + return [{"error": f"Failed to search for values: {str(e)}"}] From 18e16d0c0337bccb129b9a3bc9c5ce9ff9cb1196 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 17 Sep 2025 10:13:13 +0200 Subject: [PATCH 09/77] add localhost support for parquet also change parquetpath to path and add .parquet suffix to the path in config --- app/config.py | 4 ++-- app/services/duckdb_client.py | 20 ++++++++++++++++---- app/services/health/duckdb_queries.py | 11 ++++++----- config/.env.example | 2 +- scripts/duckdb_importer.py | 2 +- scripts/xml_exporter.py | 4 ++-- 6 files changed, 28 insertions(+), 15 deletions(-) diff --git a/app/config.py b/app/config.py index b2270dc..a2fe6c3 100644 --- a/app/config.py +++ b/app/config.py @@ -30,7 +30,7 @@ class Settings(BaseSettings): CH_DB_NAME: str = "applehealth" CH_TABLE_NAME: str = "data" - DUCKDB_FILENAME: str = "applehealth" + DUCKDB_FILENAME: str = "applehealth.parquet" CHUNK_SIZE: int = 50_000 @@ -54,7 +54,7 @@ def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str: @lru_cache def get_settings() -> Settings: - return Settings() # type: ignore[call-arg + return Settings() # type: ignore[call-arg] settings = get_settings() diff --git a/app/services/duckdb_client.py b/app/services/duckdb_client.py index c2e7659..c83c8ff 100644 --- a/app/services/duckdb_client.py +++ b/app/services/duckdb_client.py @@ -2,6 +2,7 @@ from pathlib import Path from typing import Any +import duckdb from duckdb import DuckDBPyRelation from app.config import settings @@ -9,12 +10,23 @@ @dataclass class DuckDBClient: - def __init__(self): - self.parquetpath: Path = Path(f"{settings.DUCKDB_FILENAME}.parquet") + path: Path | str = f"{settings.DUCKDB_FILENAME}" def __post_init__(self): - if not self.parquetpath.exists(): - raise FileNotFoundError(f"Parquet file not found: {self.parquetpath}") + print("__post_init__") + if self.path.startswith("localhost"): + self.path = "http://" + self.path + + if self.path.startswith(("http://", "https://")): + duckdb.sql(""" + INSTALL httpfs; + LOAD httpfs; + """) + else: + self.path = Path(self.path) + + if isinstance(self.path, Path) and not self.path.exists(): + raise FileNotFoundError(f"Parquet file not found: {self.path}") @staticmethod def format_response(response: DuckDBPyRelation) -> list[dict[str, Any]]: diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 72b8bcf..78686c9 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -11,7 +11,8 @@ def get_health_summary_from_duckdb() -> list[dict[str, Any]]: response = duckdb.sql( - f"SELECT type, COUNT(*) AS count FROM read_parquet('{client.parquetpath}') GROUP BY ALL", + f"""SELECT type, COUNT(*) AS count FROM read_parquet('{client.path}')" + GROUP BY type ORDER BY count DESC""", ) return client.format_response(response) @@ -19,7 +20,7 @@ def get_health_summary_from_duckdb() -> list[dict[str, Any]]: def search_health_records_from_duckdb( params: HealthRecordSearchParams, ) -> list[dict[str, Any]]: - query: str = f"SELECT * FROM read_parquet('{client.parquetpath}')" + query: str = f"SELECT * FROM read_parquet('{client.path}')" query += fill_query(params) response = duckdb.sql(query) return client.format_response(response) @@ -31,7 +32,7 @@ def get_statistics_by_type_from_duckdb( result = duckdb.sql(f""" SELECT type, COUNT(*) AS count, AVG(value) AS average, SUM(value) AS sum, MIN(value) AS min, MAX(value) AS max - FROM read_parquet('{client.parquetpath}') + FROM read_parquet('{client.path}') WHERE type = '{record_type}' GROUP BY type """) return client.format_response(result) @@ -47,7 +48,7 @@ def get_trend_data_from_duckdb( SELECT device, time_bucket(INTERVAL '1 {interval}', startDate) AS interval, AVG(value) AS average, SUM(value) AS sum, MIN(value) AS min, MAX(value) AS max, COUNT(*) AS count - FROM read_parquet('{client.parquetpath}') + FROM read_parquet('{client.path}') WHERE type = '{record_type}' {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} @@ -63,7 +64,7 @@ def search_values_from_duckdb( date_to: str | None = None, ) -> list[dict[str, Any]]: result = duckdb.sql(f""" - SELECT * FROM read_parquet('{client.parquetpath}') WHERE textvalue = '{value}' + SELECT * FROM read_parquet('{client.path}') WHERE textvalue = '{value}' {f"AND type = '{record_type}'" if record_type else ""} {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} diff --git a/config/.env.example b/config/.env.example index 7f9d2a5..035f920 100644 --- a/config/.env.example +++ b/config/.env.example @@ -4,6 +4,6 @@ ES_HOST="localhost" CH_DIRNAME="applehealth.chdb" CH_DB_NAME="applehealth" CH_TABLE_NAME="data" -DUCKDB_FILENAME="applehealth" +DUCKDB_FILENAME="applehealth.parquet" CHUNK_SIZE="50000" RAW_XML_PATH="raw.xml" diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index b39929c..138fabb 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -35,7 +35,7 @@ def exportxml(self) -> None: chunk_dfs.append(df) combined_df = pl.concat(chunk_dfs) - combined_df.write_parquet(f"{self.parquetpath}", compression="zstd") + combined_df.write_parquet(f"{self.path}", compression="zstd") for f in chunkfiles: os.remove(f) diff --git a/scripts/xml_exporter.py b/scripts/xml_exporter.py index 5fffcf1..b7d0c64 100644 --- a/scripts/xml_exporter.py +++ b/scripts/xml_exporter.py @@ -10,7 +10,7 @@ class XMLExporter: def __init__(self): - self.path: Path = Path(settings.RAW_XML_PATH) + self.xmlpath: Path = Path(settings.RAW_XML_PATH) self.chunk_size: int = settings.CHUNK_SIZE DATE_FIELDS: tuple[str, ...] = ("startDate", "endDate", "creationDate") @@ -62,7 +62,7 @@ def parse_xml(self) -> Generator[DataFrame, Any, None]: """ records: list[dict[str, Any]] = [] - for event, elem in ET.iterparse(self.path, events=("start",)): + for event, elem in ET.iterparse(self.xmlpath, events=("start",)): if elem.tag == "Record" and event == "start": if len(records) >= self.chunk_size: yield DataFrame(records).reindex(columns=self.COLUMN_NAMES) From fab22eb91507081e5a1c39009a507425bfc7c753 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 17 Sep 2025 11:44:23 +0200 Subject: [PATCH 10/77] remove debug from client --- app/services/duckdb_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/services/duckdb_client.py b/app/services/duckdb_client.py index c83c8ff..5ebb4be 100644 --- a/app/services/duckdb_client.py +++ b/app/services/duckdb_client.py @@ -13,7 +13,6 @@ class DuckDBClient: path: Path | str = f"{settings.DUCKDB_FILENAME}" def __post_init__(self): - print("__post_init__") if self.path.startswith("localhost"): self.path = "http://" + self.path From b619143de480fd6b36ae8e4151cf530b8186e47c Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 17 Sep 2025 13:33:25 +0200 Subject: [PATCH 11/77] unterminated string --- app/services/health/duckdb_queries.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 78686c9..52fe471 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -11,7 +11,7 @@ def get_health_summary_from_duckdb() -> list[dict[str, Any]]: response = duckdb.sql( - f"""SELECT type, COUNT(*) AS count FROM read_parquet('{client.path}')" + f"""SELECT type, COUNT(*) AS count FROM read_parquet('{client.path}') GROUP BY type ORDER BY count DESC""", ) return client.format_response(response) @@ -70,3 +70,6 @@ def search_values_from_duckdb( {f"AND startDate <= '{date_to}'" if date_to else ""} """) return client.format_response(result) + +if __name__=="__main__": + print(get_health_summary_from_duckdb()) \ No newline at end of file From 1c1678f2d35f8e9354012e87cc54fc7ecce89a03 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 17 Sep 2025 14:31:00 +0200 Subject: [PATCH 12/77] remove debug and add fileserver example --- app/services/health/duckdb_queries.py | 3 --- tests/fileserver.py | 32 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/fileserver.py diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 52fe471..4cd9937 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -70,6 +70,3 @@ def search_values_from_duckdb( {f"AND startDate <= '{date_to}'" if date_to else ""} """) return client.format_response(result) - -if __name__=="__main__": - print(get_health_summary_from_duckdb()) \ No newline at end of file diff --git a/tests/fileserver.py b/tests/fileserver.py new file mode 100644 index 0000000..462d82d --- /dev/null +++ b/tests/fileserver.py @@ -0,0 +1,32 @@ +import argparse + +import uvicorn +from fastapi import FastAPI +from fastapi.responses import FileResponse + +app = FastAPI() + + +@app.get("/{filename}") +async def serve_file(filename: str) -> FileResponse: + return FileResponse(filename) + + +parser = argparse.ArgumentParser( + prog="Filesystem server", + description="Host local files in this directory on localhost", +) +parser.add_argument( + "-p", + "--port", + type=int, + help="Port on which to serve", + default=8080, + dest="port", + action="store", +) + +if __name__ == "__main__": + args = parser.parse_args() + port = args.port + uvicorn.run(app, host="localhost", port=port) From bd0bb509a4f6854613c07efb909a5b76730921f1 Mon Sep 17 00:00:00 2001 From: Jakub Czajka Date: Wed, 17 Sep 2025 14:37:53 +0200 Subject: [PATCH 13/77] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 6db3c71..0e8064d 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,10 @@ Follow these steps to set up Apple Health MCP Server in your environment. 4. Lastly, if you're going to be using DuckDB: - Run `make duckdb` to create a parquet file with your exported XML data + - If you want to connect to the file through http(s): + - The only thing you need to do is change the .env path, e.g. `localhost:8080/applehealth.parquet` + - If you want an example on how to host the files locally, run `uv run tests/fileserver.py` + ### Configuration Files @@ -235,6 +239,7 @@ The Apple Health MCP Server provides a suite of tools for exploring, searching, | `search_health_records_es` | Flexible search for health records in Elasticsearch with advanced filtering and query options. | | `get_statistics_by_type_es` | Get comprehensive statistics (count, min, max, avg, sum) for a specific health record type. | | `get_trend_data_es` | Analyze trends for a health record type over time (daily, weekly, monthly, yearly aggregations). | +| `search_values_es` | Search for records with exactly matching values (including text). | ### ClickHouse Tools (`ch_reader`) @@ -244,6 +249,7 @@ The Apple Health MCP Server provides a suite of tools for exploring, searching, | `search_health_records_ch` | Flexible search for health records in ClickHouse with advanced filtering and query options. | | `get_statistics_by_type_ch` | Get comprehensive statistics (count, min, max, avg, sum) for a specific health record type. | | `get_trend_data_ch` | Analyze trends for a health record type over time (daily, weekly, monthly, yearly aggregations). | +| `search_values_ch` | Search for records with exactly matching values (including text). | ### DuckDB Tools (`duckdb_reader`) @@ -253,6 +259,7 @@ The Apple Health MCP Server provides a suite of tools for exploring, searching, | `search_health_records_duckdb` | Flexible search for health records in DuckDB with advanced filtering and query options. | | `get_statistics_by_type_duckdb` | Get comprehensive statistics (count, min, max, avg, sum) for a specific health record type. | | `get_trend_data_duckdb` | Analyze trends for a health record type over time (daily, weekly, monthly, yearly aggregations). | +| `search_values_duckdb` | Search for records with exactly matching values (including text). | All tools are accessible via MCP-compatible clients and can be used with natural language or programmatic queries to explore and analyze your Apple Health data. From 664bbad1e55e7e93c26e0fd9fba1a0fe4b6ac786 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 17 Sep 2025 15:44:00 +0200 Subject: [PATCH 14/77] add fastapi to dev group --- pyproject.toml | 1 + uv.lock | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4a9a28a..8d5bcc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ module-name = "app" [dependency-groups] dev = [ + "fastapi>=0.116.2", "pytest>=8.4.1", "pytest-asyncio>=1.0.0", "pytest-cov>=6.2.1", diff --git a/uv.lock b/uv.lock index c8c0248..e2b9d04 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" [[package]] @@ -52,6 +52,7 @@ code-quality = [ { name = "ty" }, ] dev = [ + { name = "fastapi" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -82,6 +83,7 @@ code-quality = [ { name = "ty", specifier = ">=0.0.1a20" }, ] dev = [ + { name = "fastapi", specifier = ">=0.116.2" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=6.2.1" }, @@ -455,6 +457,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "fastapi" +version = "0.116.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/64/1296f46d6b9e3b23fb22e5d01af3f104ef411425531376212f1eefa2794d/fastapi-0.116.2.tar.gz", hash = "sha256:231a6af2fe21cfa2c32730170ad8514985fc250bec16c9b242d3b94c835ef529", size = 298595, upload-time = "2025-09-16T18:29:23.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/e4/c543271a8018874b7f682bf6156863c416e1334b8ed3e51a69495c5d4360/fastapi-0.116.2-py3-none-any.whl", hash = "sha256:c3a7a8fb830b05f7e087d920e0d786ca1fc9892eb4e9a84b227be4c1bc7569db", size = 95670, upload-time = "2025-09-16T18:29:21.329Z" }, +] + [[package]] name = "fastmcp" version = "2.12.2" From 60e64bb79d9cc4fcc4703d016b3d404163260681 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 11:20:09 +0200 Subject: [PATCH 15/77] workouts and stats added as pq files --- app/services/health/duckdb_queries.py | 22 +++++-- scripts/duckdb_importer.py | 86 +++++++++++++++++++++----- scripts/xml_exporter.py | 88 +++++++++++++++++++++------ 3 files changed, 158 insertions(+), 38 deletions(-) diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 4cd9937..58ea5e0 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -1,6 +1,8 @@ from typing import Any import duckdb +from duckdb import DuckDBPyRelation +from fastmcp.server.dependencies import get_context from app.schemas.record import HealthRecordSearchParams, IntervalType, RecordType from app.services.duckdb_client import DuckDBClient @@ -10,10 +12,19 @@ def get_health_summary_from_duckdb() -> list[dict[str, Any]]: - response = duckdb.sql( - f"""SELECT type, COUNT(*) AS count FROM read_parquet('{client.path}') - GROUP BY type ORDER BY count DESC""", - ) + try: + response = duckdb.sql( + f"""SELECT type, COUNT(*) AS count FROM read_parquet('{client.path}') + GROUP BY type ORDER BY count DESC""", + ) + except duckdb.IOException: + try: + ctx = get_context() + ctx.error("Failed to connect to DuckDB") + except RuntimeError: + print("Failed to connect to DuckDB") + return [{'status_code': 400, 'error': 'failed to connect to DuckDB'}] + return client.format_response(response) @@ -70,3 +81,6 @@ def search_values_from_duckdb( {f"AND startDate <= '{date_to}'" if date_to else ""} """) return client.format_response(result) + +if __name__ == "__main__": + print(get_health_summary_from_duckdb()) \ No newline at end of file diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index 138fabb..7f4bfed 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -1,7 +1,9 @@ import os from pathlib import Path +import pandas as pd import polars as pl +import duckdb from app.services.duckdb_client import DuckDBClient from scripts.xml_exporter import XMLExporter @@ -12,34 +14,86 @@ def __init__(self): XMLExporter.__init__(self) DuckDBClient.__init__(self) + chunk_files = [] + + def write_to_file(self, index: int, df: pl.DataFrame) -> None: + try: + if 'workoutActivityType' in df.columns: + chunk_file: Path = Path(f"workouts.chunk_{index}.parquet") + print(f"processed {index * self.chunk_size} docs") + df.write_parquet(chunk_file, compression="zstd", compression_level=1) + self.chunk_files.append(chunk_file) + elif 'type' in df.columns: + chunk_file: Path = Path(f"records.chunk_{index}.parquet") + print(f"processed {index * self.chunk_size} docs") + df.write_parquet(chunk_file, compression="zstd", compression_level=1) + self.chunk_files.append(chunk_file) + else: + for chunk_file in self.chunk_files: + os.remove(chunk_file) + raise RuntimeError("Missing required fields in export file") + except Exception: + for file in self.chunk_files: + os.remove(file) + raise RuntimeError(f"Failed to write chunk file to disk: {chunk_file}") + def exportxml(self) -> None: - chunkfiles = [] for i, docs in enumerate(self.parse_xml(), 1): df: pl.DataFrame = pl.DataFrame(docs) - chunk_file: Path = Path(f"data.chunk_{i}.parquet") - print(f"processed {i * self.chunk_size} docs") - df.write_parquet(chunk_file, compression="zstd", compression_level=1) - chunkfiles.append(chunk_file) - print(f"written {i * self.chunk_size} docs") + self.write_to_file(i, df) - chunk_dfs: list[pl.DataFrame] = [] - reference_columns: list[str] = [] + record_chunk_dfs: list[pl.DataFrame] = [] + workout_chunk_dfs: list[pl.DataFrame] = [] + stat_chunk_dfs: list[pl.DataFrame] = [] + # reference_columns: list[str] = [] - for chunk_file in chunkfiles: + for chunk_file in self.chunk_files: df = pl.read_parquet(chunk_file) + if 'value' in df.columns: + df = df.select(self.RECORD_COLUMNS) + record_chunk_dfs.append(df) + elif 'device' in df.columns: + df = df.select(self.WORKOUT_COLUMNS) + workout_chunk_dfs.append(df) + elif 'sum' in df.columns: + df = df.select(self.WORKOUT_STATS_COLUMNS) + stat_chunk_dfs.append(df) - if not reference_columns: - reference_columns = df.columns - df = df.select(reference_columns) - chunk_dfs.append(df) - combined_df = pl.concat(chunk_dfs) - combined_df.write_parquet(f"{self.path}", compression="zstd") + try: + record_df = pl.concat(record_chunk_dfs) + workout_df = pl.concat(workout_chunk_dfs) + stat_df = pl.concat(stat_chunk_dfs) + except Exception as e: + for f in self.chunk_files: + os.remove(f) + raise RuntimeError(f"Failed to concatenate dataframes: {str(e)}") + try: + record_df.write_parquet(f"{self.path / "records.parquet"}", compression="zstd") + workout_df.write_parquet(f"{self.path / "workouts.parquet"}", compression="zstd") + stat_df.write_parquet(f"{self.path / "stats.parquet"}", compression="zstd") + except Exception as e: + for f in self.chunk_files: + os.remove(f) + raise RuntimeError(f"Failed to write to path {self.path}: {str(e)}") - for f in chunkfiles: + for f in self.chunk_files: os.remove(f) + def export_to_multiple(self): + con = duckdb.connect("shitass.duckdb") + for i, docs in enumerate(self.parse_xml(), 1): + tables = docs.partition_by("type", as_dict=True) + for key, table in tables.items(): + pddf = table.to_pandas() + con.execute(f""" + CREATE TABLE IF NOT EXISTS {key[0]} + AS SELECT * FROM pddf + """) + # print(duckdb.sql(f"select * from pddf").fetchall()) + print(f"processed {i * self.chunk_size} docs") + if __name__ == "__main__": importer = ParquetImporter() diff --git a/scripts/xml_exporter.py b/scripts/xml_exporter.py index b7d0c64..12e97c9 100644 --- a/scripts/xml_exporter.py +++ b/scripts/xml_exporter.py @@ -4,6 +4,7 @@ from xml.etree import ElementTree as ET from pandas import DataFrame +import polars as pl from app.config import settings @@ -15,12 +16,12 @@ def __init__(self): DATE_FIELDS: tuple[str, ...] = ("startDate", "endDate", "creationDate") DEFAULT_VALUES: dict[str, str] = { - "unit": "unknown", - "sourceVersion": "unknown", - "device": "unknown", - "value": "unknown", + "unit": "", + "sourceVersion": "", + "device": "", + "value": "", } - COLUMN_NAMES: tuple[str, ...] = ( + RECORD_COLUMNS: tuple[str, ...] = ( "type", "sourceVersion", "sourceName", @@ -32,8 +33,24 @@ def __init__(self): "value", "textvalue", ) + WORKOUT_COLUMNS: tuple[str, ...] = ( + "type", + "duration", + "durationUnit", + "sourceName", + "startDate", + "endDate", + "creationDate", + ) + WORKOUT_STATS_COLUMNS: tuple[str, ...] = ( + "type", + "startDate", + "endDate", + "sum", + "unit", + ) - def update_record(self, document: dict[str, Any]) -> dict[str, Any]: + def update_record(self, kind: str, document: dict[str, Any]) -> dict[str, Any]: """ Updates records to fill out columns without specified data: There are 9 columns that need to be filled out, and there are 4 columns @@ -41,38 +58,73 @@ def update_record(self, document: dict[str, Any]) -> dict[str, Any]: Additionally a textvalue field is added for querying text values """ for field in self.DATE_FIELDS: - document[field] = datetime.strptime(document[field], "%Y-%m-%d %H:%M:%S %z") + if field in document: + document[field] = datetime.strptime(document[field], "%Y-%m-%d %H:%M:%S %z") + + if kind == "record": + if len(document) != 9: + document.update({k: v for k, v in self.DEFAULT_VALUES.items() if k not in document}) - if len(document) != 9: - document.update({k: v for k, v in self.DEFAULT_VALUES.items() if k not in document}) + document["textvalue"] = document["value"] - document["textvalue"] = document["value"] + try: + document["value"] = float(document["value"]) + except (TypeError, ValueError): + document["value"] = 0.0 - try: - document["value"] = float(document["value"]) - except (TypeError, ValueError): - document["value"] = 0.0 + elif kind == "workout": + document["type"] = document.pop("workoutActivityType") + + try: + document["duration"] = float(document["duration"]) + except (TypeError, ValueError): + document["duration"] = 0.0 return document - def parse_xml(self) -> Generator[DataFrame, Any, None]: + def parse_xml(self) -> Generator[pl.DataFrame, Any, None]: """ Parses the XML file and yields pandas dataframes of specified chunk_size. Extracts attributes from each Record element. """ records: list[dict[str, Any]] = [] + workouts: list[dict[str, Any]] = [] + workout_stats: list[dict[str, Any]] = [] for event, elem in ET.iterparse(self.xmlpath, events=("start",)): if elem.tag == "Record" and event == "start": if len(records) >= self.chunk_size: - yield DataFrame(records).reindex(columns=self.COLUMN_NAMES) + # yield pl.DataFrame(records) + yield DataFrame(records).reindex(columns=self.RECORD_COLUMNS) records = [] record: dict[str, Any] = elem.attrib.copy() # fill out empty cells if they exist and convert dates to datetime - self.update_record(record) + self.update_record("record", record) records.append(record) + + elif elem.tag == "Workout" and event == "start": + if len(workouts) >= self.chunk_size: + yield DataFrame(workouts).reindex(columns=self.WORKOUT_COLUMNS) + workouts = [] + workout: dict[str, Any] = elem.attrib.copy() + + for stat in elem: + if stat.tag != "WorkoutStatistics": + continue + statistic = stat.attrib.copy() + self.update_record("stat", statistic) + workout_stats.append(statistic) + if len(workout_stats) >= self.chunk_size: + yield DataFrame(workout_stats).reindex(columns=self.WORKOUT_STATS_COLUMNS) + workout_stats = [] + + self.update_record("workout", workout) + workouts.append(workout) elem.clear() # yield remaining records - yield DataFrame(records).reindex(columns=self.COLUMN_NAMES) + # yield pl.DataFrame(records) + yield DataFrame(records).reindex(columns=self.RECORD_COLUMNS) + yield DataFrame(workouts).reindex(columns=self.WORKOUT_COLUMNS) + yield DataFrame(workout_stats).reindex(columns=self.WORKOUT_STATS_COLUMNS) From e73004bfeaac7450790ad9f3cfc552eb3b58c713 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 11:49:03 +0200 Subject: [PATCH 16/77] concat check --- scripts/duckdb_importer.py | 9 ++++++--- tests/mcptest.py | 0 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 tests/mcptest.py diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index 7f4bfed..508448e 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -62,9 +62,12 @@ def exportxml(self) -> None: try: - record_df = pl.concat(record_chunk_dfs) - workout_df = pl.concat(workout_chunk_dfs) - stat_df = pl.concat(stat_chunk_dfs) + if record_chunk_dfs: + record_df = pl.concat(record_chunk_dfs) + if workout_chunk_dfs: + workout_df = pl.concat(workout_chunk_dfs) + if stat_chunk_dfs: + stat_df = pl.concat(stat_chunk_dfs) except Exception as e: for f in self.chunk_files: os.remove(f) diff --git a/tests/mcptest.py b/tests/mcptest.py new file mode 100644 index 0000000..e69de29 From 4fa826d0a6674f42e14045a63afe53d1d503f308 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 11:49:24 +0200 Subject: [PATCH 17/77] asfas --- tests/mcptest.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/mcptest.py b/tests/mcptest.py index e69de29..b148de7 100644 --- a/tests/mcptest.py +++ b/tests/mcptest.py @@ -0,0 +1,20 @@ +import pytest + +from agents import Agent, Runner +from agents.mcp import MCPServerStdio, MCPServerStdioParams + +params = MCPServerStdioParams( + command="uv", + args = [ + "run", + "--directory", + "..", + "fastmcp", + "run", + "start.py" + ] +) + +server = MCPServerStdio(params=params) + +agent = Agent() \ No newline at end of file From d60551b54609c8f7b9755e55a8fea0bf1e65f04a Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 12:03:42 +0200 Subject: [PATCH 18/77] import fix --- pyproject.toml | 2 + scripts/duckdb_importer.py | 13 ++-- uv.lock | 131 +++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d5bcc0..0794c71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,8 @@ module-name = "app" [dependency-groups] dev = [ "fastapi>=0.116.2", + "nest-asyncio>=1.6.0", + "openai-agents>=0.3.2", "pytest>=8.4.1", "pytest-asyncio>=1.0.0", "pytest-cov>=6.2.1", diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index 508448e..d320331 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -59,7 +59,9 @@ def exportxml(self) -> None: df = df.select(self.WORKOUT_STATS_COLUMNS) stat_chunk_dfs.append(df) - + record_df = None + workout_df = None + stat_df = None try: if record_chunk_dfs: @@ -73,9 +75,12 @@ def exportxml(self) -> None: os.remove(f) raise RuntimeError(f"Failed to concatenate dataframes: {str(e)}") try: - record_df.write_parquet(f"{self.path / "records.parquet"}", compression="zstd") - workout_df.write_parquet(f"{self.path / "workouts.parquet"}", compression="zstd") - stat_df.write_parquet(f"{self.path / "stats.parquet"}", compression="zstd") + if record_df: + record_df.write_parquet(f"{self.path / "records.parquet"}", compression="zstd") + if workout_df: + workout_df.write_parquet(f"{self.path / "workouts.parquet"}", compression="zstd") + if stat_df: + stat_df.write_parquet(f"{self.path / "stats.parquet"}", compression="zstd") except Exception as e: for f in self.chunk_files: os.remove(f) diff --git a/uv.lock b/uv.lock index e2b9d04..3df2b2d 100644 --- a/uv.lock +++ b/uv.lock @@ -53,6 +53,8 @@ code-quality = [ ] dev = [ { name = "fastapi" }, + { name = "nest-asyncio" }, + { name = "openai-agents" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -84,6 +86,8 @@ code-quality = [ ] dev = [ { name = "fastapi", specifier = ">=0.116.2" }, + { name = "nest-asyncio", specifier = ">=1.6.0" }, + { name = "openai-agents", specifier = ">=0.3.2" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=6.2.1" }, @@ -352,6 +356,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -526,6 +539,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -608,6 +633,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] +[[package]] +name = "jiter" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/c0/a3bb4cc13aced219dd18191ea66e874266bd8aa7b96744e495e1c733aa2d/jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4", size = 167094, upload-time = "2025-09-15T09:20:38.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/c4/d530e514d0f4f29b2b68145e7b389cbc7cac7f9c8c23df43b04d3d10fa3e/jiter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb", size = 305021, upload-time = "2025-09-15T09:19:43.523Z" }, + { url = "https://files.pythonhosted.org/packages/7a/77/796a19c567c5734cbfc736a6f987affc0d5f240af8e12063c0fb93990ffa/jiter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471", size = 314384, upload-time = "2025-09-15T09:19:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/14/9c/824334de0b037b91b6f3fa9fe5a191c83977c7ec4abe17795d3cb6d174cf/jiter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd", size = 337389, upload-time = "2025-09-15T09:19:46.094Z" }, + { url = "https://files.pythonhosted.org/packages/a2/95/ed4feab69e6cf9b2176ea29d4ef9d01a01db210a3a2c8a31a44ecdc68c38/jiter-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921", size = 360519, upload-time = "2025-09-15T09:19:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0c/2ad00f38d3e583caba3909d95b7da1c3a7cd82c0aa81ff4317a8016fb581/jiter-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df", size = 487198, upload-time = "2025-09-15T09:19:49.116Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/919b64cf3499b79bdfba6036da7b0cac5d62d5c75a28fb45bad7819e22f0/jiter-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982", size = 377835, upload-time = "2025-09-15T09:19:50.468Z" }, + { url = "https://files.pythonhosted.org/packages/29/7f/8ebe15b6e0a8026b0d286c083b553779b4dd63db35b43a3f171b544de91d/jiter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64", size = 347655, upload-time = "2025-09-15T09:19:51.726Z" }, + { url = "https://files.pythonhosted.org/packages/8e/64/332127cef7e94ac75719dda07b9a472af6158ba819088d87f17f3226a769/jiter-0.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1", size = 386135, upload-time = "2025-09-15T09:19:53.075Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/557b63527442f84c14774159948262a9d4fabb0d61166f11568f22fc60d2/jiter-0.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758", size = 516063, upload-time = "2025-09-15T09:19:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/86/13/4164c819df4a43cdc8047f9a42880f0ceef5afeb22e8b9675c0528ebdccd/jiter-0.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166", size = 508139, upload-time = "2025-09-15T09:19:55.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/70/6e06929b401b331d41ddb4afb9f91cd1168218e3371972f0afa51c9f3c31/jiter-0.11.0-cp313-cp313-win32.whl", hash = "sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80", size = 206369, upload-time = "2025-09-15T09:19:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0d/8185b8e15de6dce24f6afae63380e16377dd75686d56007baa4f29723ea1/jiter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6", size = 202538, upload-time = "2025-09-15T09:19:58.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/3a/d61707803260d59520721fa326babfae25e9573a88d8b7b9cb54c5423a59/jiter-0.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33", size = 313737, upload-time = "2025-09-15T09:19:59.638Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cc/c9f0eec5d00f2a1da89f6bdfac12b8afdf8d5ad974184863c75060026457/jiter-0.11.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03", size = 346183, upload-time = "2025-09-15T09:20:01.442Z" }, + { url = "https://files.pythonhosted.org/packages/a6/87/fc632776344e7aabbab05a95a0075476f418c5d29ab0f2eec672b7a1f0ac/jiter-0.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba", size = 204225, upload-time = "2025-09-15T09:20:03.102Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/e7f45be7d3969bdf2e3cd4b816a7a1d272507cd0edd2d6dc4b07514f2d9a/jiter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72", size = 304414, upload-time = "2025-09-15T09:20:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/06/32/13e8e0d152631fcc1907ceb4943711471be70496d14888ec6e92034e2caf/jiter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774", size = 314223, upload-time = "2025-09-15T09:20:05.631Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/abedd5b5a20ca083f778d96bba0d2366567fcecb0e6e34ff42640d5d7a18/jiter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0", size = 337306, upload-time = "2025-09-15T09:20:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/30d59bdc1204c86aa975ec72c48c482fee6633120ee9c3ab755e4dfefea8/jiter-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a", size = 360565, upload-time = "2025-09-15T09:20:08.283Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/567288e0d2ed9fa8f7a3b425fdaf2cb82b998633c24fe0d98f5417321aa8/jiter-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773", size = 486465, upload-time = "2025-09-15T09:20:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/18/6e/7b72d09273214cadd15970e91dd5ed9634bee605176107db21e1e4205eb1/jiter-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7", size = 377581, upload-time = "2025-09-15T09:20:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/58/52/4db456319f9d14deed325f70102577492e9d7e87cf7097bda9769a1fcacb/jiter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2", size = 347102, upload-time = "2025-09-15T09:20:12.175Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b4/433d5703c38b26083aec7a733eb5be96f9c6085d0e270a87ca6482cbf049/jiter-0.11.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2", size = 386477, upload-time = "2025-09-15T09:20:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7a/a60bfd9c55b55b07c5c441c5085f06420b6d493ce9db28d069cc5b45d9f3/jiter-0.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0", size = 516004, upload-time = "2025-09-15T09:20:14.848Z" }, + { url = "https://files.pythonhosted.org/packages/2e/46/f8363e5ecc179b4ed0ca6cb0a6d3bfc266078578c71ff30642ea2ce2f203/jiter-0.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73", size = 507855, upload-time = "2025-09-15T09:20:16.176Z" }, + { url = "https://files.pythonhosted.org/packages/90/33/396083357d51d7ff0f9805852c288af47480d30dd31d8abc74909b020761/jiter-0.11.0-cp314-cp314-win32.whl", hash = "sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2", size = 205802, upload-time = "2025-09-15T09:20:17.661Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/eb06ca556b2551d41de7d03bf2ee24285fa3d0c58c5f8d95c64c9c3281b1/jiter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40", size = 313405, upload-time = "2025-09-15T09:20:18.918Z" }, + { url = "https://files.pythonhosted.org/packages/af/22/7ab7b4ec3a1c1f03aef376af11d23b05abcca3fb31fbca1e7557053b1ba2/jiter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406", size = 347102, upload-time = "2025-09-15T09:20:20.16Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" @@ -756,6 +817,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -817,6 +887,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" }, ] +[[package]] +name = "openai" +version = "1.109.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/9f/d11cc7fb2d60af14a97dbef4e3c7b23917387995c257951fdc321d8efd0a/openai-1.109.0.tar.gz", hash = "sha256:701e26d13e3953524ba99f44cf5fbbda40eafd41ba15a8d85b76229a2693cfe5", size = 563971, upload-time = "2025-09-23T16:59:41.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/d3/d65e318e8d3b5845afaa5960d8da779988b4cb9f4e1ca82e588fed9c6a9d/openai-1.109.0-py3-none-any.whl", hash = "sha256:8c0910bdd4ee1274d5ff0354786bdd0bc79e68c158d5d2c19e24208b412e5792", size = 948421, upload-time = "2025-09-23T16:59:39.516Z" }, +] + +[[package]] +name = "openai-agents" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mcp" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/9f/dafa9f80653778179822e1abf77c7f0d9da5a16806c96b5bb9e0e46bd747/openai_agents-0.3.2.tar.gz", hash = "sha256:b71ac04ee9f502f1bc0f4d142407df4ec69db4442db86c4da252b4558fa90cd5", size = 1727988, upload-time = "2025-09-23T20:37:20.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/7e/6a8437f9f40937bb473ceb120a65e1b37bc87bcee6da67be4c05b25c6a89/openai_agents-0.3.2-py3-none-any.whl", hash = "sha256:55e02c57f2aaf3170ff0aa0ab7c337c28fd06b43b3bb9edc28b77ffd8142b425", size = 194221, upload-time = "2025-09-23T20:37:19.121Z" }, +] + [[package]] name = "openapi-core" version = "0.19.5" @@ -1489,6 +1596,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/ac/03bbe688c090b5e16b507b4e36d7c4e5d95e2a0861dd77922801088edfb1/testcontainers_postgres-0.0.1rc1-py3-none-any.whl", hash = "sha256:1bd0afcff2c236c08ffbf3e4926e713d8c58e20df82c31e62fb9cca70582fd5a", size = 2906, upload-time = "2023-01-06T16:37:45.675Z" }, ] +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + [[package]] name = "ty" version = "0.0.1a20" @@ -1514,6 +1633,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/36/5a3a70c5d497d3332f9e63cabc9c6f13484783b832fecc393f4f1c0c4aa8/ty-0.0.1a20-py3-none-win_arm64.whl", hash = "sha256:d8ac1c5a14cda5fad1a8b53959d9a5d979fe16ce1cc2785ea8676fed143ac85f", size = 8269906, upload-time = "2025-09-03T12:35:45.045Z" }, ] +[[package]] +name = "types-requests" +version = "2.32.4.20250913" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" From fabea706f01f3fd3eb5411e2c13444e85196cbae Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 12:32:29 +0200 Subject: [PATCH 19/77] is nto noene --- scripts/duckdb_importer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index d320331..da887e5 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -75,11 +75,11 @@ def exportxml(self) -> None: os.remove(f) raise RuntimeError(f"Failed to concatenate dataframes: {str(e)}") try: - if record_df: + if record_df is not None: record_df.write_parquet(f"{self.path / "records.parquet"}", compression="zstd") - if workout_df: + if workout_df is not None: workout_df.write_parquet(f"{self.path / "workouts.parquet"}", compression="zstd") - if stat_df: + if stat_df is not None: stat_df.write_parquet(f"{self.path / "stats.parquet"}", compression="zstd") except Exception as e: for f in self.chunk_files: From 554d9a55c285d38737149981bf39f1792b3631c3 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 14:41:01 +0200 Subject: [PATCH 20/77] tests --- tests/import_tests.py | 4 + tests/mcp.json | 9 ++ tests/mcptest.py | 62 +++++++++---- tests/output.json | 205 ++++++++++++++++++++++++++++++++++++++++++ tests/query_tests.py | 10 +++ 5 files changed, 275 insertions(+), 15 deletions(-) create mode 100644 tests/import_tests.py create mode 100644 tests/mcp.json create mode 100644 tests/output.json create mode 100644 tests/query_tests.py diff --git a/tests/import_tests.py b/tests/import_tests.py new file mode 100644 index 0000000..f21b633 --- /dev/null +++ b/tests/import_tests.py @@ -0,0 +1,4 @@ +import pytest + +from scripts import duckdb_importer + diff --git a/tests/mcp.json b/tests/mcp.json new file mode 100644 index 0000000..56fcdc0 --- /dev/null +++ b/tests/mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "apple": { + "type": "streamable-http", + "url": "http://localhost:8000/mcp", + "note": "For Streamable HTTP connections, add this URL directly in your MCP Client" + } + } +} \ No newline at end of file diff --git a/tests/mcptest.py b/tests/mcptest.py index b148de7..7d121d0 100644 --- a/tests/mcptest.py +++ b/tests/mcptest.py @@ -1,20 +1,52 @@ -import pytest +import asyncio +import nest_asyncio from agents import Agent, Runner -from agents.mcp import MCPServerStdio, MCPServerStdioParams - -params = MCPServerStdioParams( - command="uv", - args = [ - "run", - "--directory", - "..", - "fastmcp", - "run", - "start.py" - ] +from agents.mcp import ( + MCPServerStreamableHttp, + MCPServerStreamableHttpParams, ) +from agents.model_settings import ModelSettings +from dotenv import load_dotenv -server = MCPServerStdio(params=params) -agent = Agent() \ No newline at end of file +load_dotenv() +nest_asyncio.apply() + + +async def main() -> None: + # params = MCPServerStdioParams( + # command="uv", + # args = [ + # "run", + # "--directory", + # "../app", + # "fastmcp", + # "run", + # "main.py" + # ] + # ) + # + # async with MCPServerStdio( + # name="apple serwer", + # params=params + # ) as server: + + params = MCPServerStreamableHttpParams( + url="http://localhost:8000/mcp/", + ) + + async with MCPServerStreamableHttp( + params=params, + ) as server: + agent = Agent( + name="cwel", + mcp_servers=[server], + model_settings=ModelSettings(tool_choice="required"), + ) + result = await Runner.run(agent, "give me a health summary from duckdb") + print(result.final_output) + + + +asyncio.run(main()) diff --git a/tests/output.json b/tests/output.json new file mode 100644 index 0000000..c13c11a --- /dev/null +++ b/tests/output.json @@ -0,0 +1,205 @@ +{ + "content": [ + { + "type": "text", + "text": "[{\"type\":\"HKQuantityTypeIdentifierActiveEnergyBurned\",\"count\":853486},{\"type\":\"HKQuantityTypeIdentifierHeartRate\",\"count\":411634},{\"type\":\"HKQuantityTypeIdentifierBasalEnergyBurned\",\"count\":297838},{\"type\":\"HKQuantityTypeIdentifierDistanceWalkingRunning\",\"count\":291024},{\"type\":\"HKQuantityTypeIdentifierStepCount\",\"count\":249130},{\"type\":\"HKQuantityTypeIdentifierRunningSpeed\",\"count\":80848},{\"type\":\"HKQuantityTypeIdentifierAppleExerciseTime\",\"count\":73715},{\"type\":\"HKQuantityTypeIdentifierRunningPower\",\"count\":68738},{\"type\":\"HKQuantityTypeIdentifierAppleStandTime\",\"count\":45524},{\"type\":\"HKQuantityTypeIdentifierFlightsClimbed\",\"count\":44125},{\"type\":\"HKQuantityTypeIdentifierWalkingStepLength\",\"count\":41021},{\"type\":\"HKQuantityTypeIdentifierWalkingSpeed\",\"count\":41016},{\"type\":\"HKQuantityTypeIdentifierEnvironmentalAudioExposure\",\"count\":40515},{\"type\":\"HKQuantityTypeIdentifierRunningVerticalOscillation\",\"count\":33382},{\"type\":\"HKQuantityTypeIdentifierRunningStrideLength\",\"count\":31271},{\"type\":\"HKQuantityTypeIdentifierWalkingDoubleSupportPercentage\",\"count\":30273},{\"type\":\"HKQuantityTypeIdentifierRunningGroundContactTime\",\"count\":28981},{\"type\":\"HKCategoryTypeIdentifierAppleStandHour\",\"count\":24515},{\"type\":\"HKQuantityTypeIdentifierRespiratoryRate\",\"count\":23939},{\"type\":\"HKQuantityTypeIdentifierHeadphoneAudioExposure\",\"count\":18757},{\"type\":\"HKQuantityTypeIdentifierStairDescentSpeed\",\"count\":14880},{\"type\":\"HKQuantityTypeIdentifierOxygenSaturation\",\"count\":13319},{\"type\":\"HKQuantityTypeIdentifierWalkingAsymmetryPercentage\",\"count\":9659},{\"type\":\"HKQuantityTypeIdentifierStairAscentSpeed\",\"count\":9527},{\"type\":\"HKQuantityTypeIdentifierHeartRateVariabilitySDNN\",\"count\":6904},{\"type\":\"HKQuantityTypeIdentifierBodyMass\",\"count\":3747},{\"type\":\"HKQuantityTypeIdentifierBodyMassIndex\",\"count\":3344},{\"type\":\"HKQuantityTypeIdentifierRestingHeartRate\",\"count\":894},{\"type\":\"HKQuantityTypeIdentifierWalkingHeartRateAverage\",\"count\":877},{\"type\":\"HKQuantityTypeIdentifierVO2Max\",\"count\":739},{\"type\":\"HKQuantityTypeIdentifierEnvironmentalSoundReduction\",\"count\":632},{\"type\":\"HKCategoryTypeIdentifierAudioExposureEvent\",\"count\":250},{\"type\":\"HKQuantityTypeIdentifierSixMinuteWalkTestDistance\",\"count\":104},{\"type\":\"HKCategoryTypeIdentifierHandwashingEvent\",\"count\":86},{\"type\":\"HKCategoryTypeIdentifierMindfulSession\",\"count\":84},{\"type\":\"HKQuantityTypeIdentifierAppleWalkingSteadiness\",\"count\":73},{\"type\":\"HKQuantityTypeIdentifierHeartRateRecoveryOneMinute\",\"count\":67},{\"type\":\"HKQuantityTypeIdentifierDistanceCycling\",\"count\":49},{\"type\":\"HKQuantityTypeIdentifierDistanceSwimming\",\"count\":39},{\"type\":\"HKQuantityTypeIdentifierSwimmingStrokeCount\",\"count\":39},{\"type\":\"HKCategoryTypeIdentifierHeadphoneAudioExposureEvent\",\"count\":13},{\"type\":\"HKQuantityTypeIdentifierDietaryEnergyConsumed\",\"count\":6},{\"type\":\"HKCategoryTypeIdentifierHighHeartRateEvent\",\"count\":5},{\"type\":\"HKCategoryTypeIdentifierLowHeartRateEvent\",\"count\":4},{\"type\":\"HKQuantityTypeIdentifierHeight\",\"count\":2},{\"type\":\"HKQuantityTypeIdentifierDietaryWater\",\"count\":1},{\"type\":\"HKDataTypeSleepDurationGoal\",\"count\":1},{\"type\":\"HKQuantityTypeIdentifierNumberOfTimesFallen\",\"count\":1}]" + } + ], + "structuredContent": { + "result": [ + { + "type": "HKQuantityTypeIdentifierActiveEnergyBurned", + "count": 853486 + }, + { + "type": "HKQuantityTypeIdentifierHeartRate", + "count": 411634 + }, + { + "type": "HKQuantityTypeIdentifierBasalEnergyBurned", + "count": 297838 + }, + { + "type": "HKQuantityTypeIdentifierDistanceWalkingRunning", + "count": 291024 + }, + { + "type": "HKQuantityTypeIdentifierStepCount", + "count": 249130 + }, + { + "type": "HKQuantityTypeIdentifierRunningSpeed", + "count": 80848 + }, + { + "type": "HKQuantityTypeIdentifierAppleExerciseTime", + "count": 73715 + }, + { + "type": "HKQuantityTypeIdentifierRunningPower", + "count": 68738 + }, + { + "type": "HKQuantityTypeIdentifierAppleStandTime", + "count": 45524 + }, + { + "type": "HKQuantityTypeIdentifierFlightsClimbed", + "count": 44125 + }, + { + "type": "HKQuantityTypeIdentifierWalkingStepLength", + "count": 41021 + }, + { + "type": "HKQuantityTypeIdentifierWalkingSpeed", + "count": 41016 + }, + { + "type": "HKQuantityTypeIdentifierEnvironmentalAudioExposure", + "count": 40515 + }, + { + "type": "HKQuantityTypeIdentifierRunningVerticalOscillation", + "count": 33382 + }, + { + "type": "HKQuantityTypeIdentifierRunningStrideLength", + "count": 31271 + }, + { + "type": "HKQuantityTypeIdentifierWalkingDoubleSupportPercentage", + "count": 30273 + }, + { + "type": "HKQuantityTypeIdentifierRunningGroundContactTime", + "count": 28981 + }, + { + "type": "HKCategoryTypeIdentifierAppleStandHour", + "count": 24515 + }, + { + "type": "HKQuantityTypeIdentifierRespiratoryRate", + "count": 23939 + }, + { + "type": "HKQuantityTypeIdentifierHeadphoneAudioExposure", + "count": 18757 + }, + { + "type": "HKQuantityTypeIdentifierStairDescentSpeed", + "count": 14880 + }, + { + "type": "HKQuantityTypeIdentifierOxygenSaturation", + "count": 13319 + }, + { + "type": "HKQuantityTypeIdentifierWalkingAsymmetryPercentage", + "count": 9659 + }, + { + "type": "HKQuantityTypeIdentifierStairAscentSpeed", + "count": 9527 + }, + { + "type": "HKQuantityTypeIdentifierHeartRateVariabilitySDNN", + "count": 6904 + }, + { + "type": "HKQuantityTypeIdentifierBodyMass", + "count": 3747 + }, + { + "type": "HKQuantityTypeIdentifierBodyMassIndex", + "count": 3344 + }, + { + "type": "HKQuantityTypeIdentifierRestingHeartRate", + "count": 894 + }, + { + "type": "HKQuantityTypeIdentifierWalkingHeartRateAverage", + "count": 877 + }, + { + "type": "HKQuantityTypeIdentifierVO2Max", + "count": 739 + }, + { + "type": "HKQuantityTypeIdentifierEnvironmentalSoundReduction", + "count": 632 + }, + { + "type": "HKCategoryTypeIdentifierAudioExposureEvent", + "count": 250 + }, + { + "type": "HKQuantityTypeIdentifierSixMinuteWalkTestDistance", + "count": 104 + }, + { + "type": "HKCategoryTypeIdentifierHandwashingEvent", + "count": 86 + }, + { + "type": "HKCategoryTypeIdentifierMindfulSession", + "count": 84 + }, + { + "type": "HKQuantityTypeIdentifierAppleWalkingSteadiness", + "count": 73 + }, + { + "type": "HKQuantityTypeIdentifierHeartRateRecoveryOneMinute", + "count": 67 + }, + { + "type": "HKQuantityTypeIdentifierDistanceCycling", + "count": 49 + }, + { + "type": "HKQuantityTypeIdentifierDistanceSwimming", + "count": 39 + }, + { + "type": "HKQuantityTypeIdentifierSwimmingStrokeCount", + "count": 39 + }, + { + "type": "HKCategoryTypeIdentifierHeadphoneAudioExposureEvent", + "count": 13 + }, + { + "type": "HKQuantityTypeIdentifierDietaryEnergyConsumed", + "count": 6 + }, + { + "type": "HKCategoryTypeIdentifierHighHeartRateEvent", + "count": 5 + }, + { + "type": "HKCategoryTypeIdentifierLowHeartRateEvent", + "count": 4 + }, + { + "type": "HKQuantityTypeIdentifierHeight", + "count": 2 + }, + { + "type": "HKQuantityTypeIdentifierDietaryWater", + "count": 1 + }, + { + "type": "HKDataTypeSleepDurationGoal", + "count": 1 + }, + { + "type": "HKQuantityTypeIdentifierNumberOfTimesFallen", + "count": 1 + } + ] + }, + "isError": false +} \ No newline at end of file diff --git a/tests/query_tests.py b/tests/query_tests.py new file mode 100644 index 0000000..50b375f --- /dev/null +++ b/tests/query_tests.py @@ -0,0 +1,10 @@ +import pytest + +from app.services.health.duckdb_queries import ( + get_health_summary_from_duckdb, + search_health_records_from_duckdb, + get_statistics_by_type_from_duckdb, + get_trend_data_from_duckdb, + search_values_from_duckdb +) + From 5b928573c2682710e34733b5ecabc4d67dc87435 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 16:06:13 +0200 Subject: [PATCH 21/77] order by sourcename + add unit tests for all queries from duckdb --- app/services/health/duckdb_queries.py | 7 ++- tests/query_tests.py | 83 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 58ea5e0..3681f1b 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -23,7 +23,8 @@ def get_health_summary_from_duckdb() -> list[dict[str, Any]]: ctx.error("Failed to connect to DuckDB") except RuntimeError: print("Failed to connect to DuckDB") - return [{'status_code': 400, 'error': 'failed to connect to DuckDB'}] + return [{'status_code': 400, 'error': 'failed to connect to DuckDB', + 'path': client.path}] return client.format_response(response) @@ -56,14 +57,14 @@ def get_trend_data_from_duckdb( date_to: str | None = None, ) -> list[dict[str, Any]]: result = duckdb.sql(f""" - SELECT device, time_bucket(INTERVAL '1 {interval}', startDate) AS interval, + SELECT sourceName, time_bucket(INTERVAL '1 {interval}', startDate) AS interval, AVG(value) AS average, SUM(value) AS sum, MIN(value) AS min, MAX(value) AS max, COUNT(*) AS count FROM read_parquet('{client.path}') WHERE type = '{record_type}' {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} - GROUP BY interval, device ORDER BY interval ASC + GROUP BY interval, sourceName ORDER BY interval ASC """) return client.format_response(result) diff --git a/tests/query_tests.py b/tests/query_tests.py index 50b375f..875da55 100644 --- a/tests/query_tests.py +++ b/tests/query_tests.py @@ -1,5 +1,11 @@ +import os +from pathlib import Path + import pytest +path = Path(__file__).parent / "records.parquet" +os.environ["DUCKDB_FILENAME"] = str(path) + from app.services.health.duckdb_queries import ( get_health_summary_from_duckdb, search_health_records_from_duckdb, @@ -7,4 +13,81 @@ get_trend_data_from_duckdb, search_values_from_duckdb ) +from app.schemas.record import HealthRecordSearchParams + + +@pytest.fixture +def counts(): + return { + 'HKQuantityTypeIdentifierBasalEnergyBurned': 18, + 'HKQuantityTypeIdentifierStepCount': 10, + 'HKQuantityTypeIdentifierHeartRate': 17, + 'HKQuantityTypeIdentifierBodyMassIndex': 8, + 'HKQuantityTypeIdentifierDietaryWater': 1, + } + +@pytest.fixture +def summary(): + return get_health_summary_from_duckdb() + +@pytest.fixture +def records(): + return search_health_records_from_duckdb(HealthRecordSearchParams( + record_type="HKQuantityTypeIdentifierStepCount", + value_min = "65", + value_max = "90", + )) + +@pytest.fixture +def statistics(): + return get_statistics_by_type_from_duckdb( + "HKQuantityTypeIdentifierStepCount" + ) + +@pytest.fixture +def trend_data(): + return get_trend_data_from_duckdb( + record_type = "HKQuantityTypeIdentifierBasalEnergyBurned" + ) + +@pytest.fixture +def value_search(): + return search_values_from_duckdb( + record_type = "HKQuantityTypeIdentifierStepCount", + value = "13" + ) + + + +def test_summary(summary, counts): + assert len(summary) == 5 + for record in summary: + assert record['count'] == counts[record['type']] + +def test_records(records): + assert len(records) == 3 + for record in records: + assert 65 < record['value'] < 90 + assert record['type'] == "HKQuantityTypeIdentifierStepCount" + +def test_statistics(statistics): + assert len(statistics) == 1 + # turn list containing 1 dict into a dict + statistics = statistics[0] + assert statistics['min'] == 3 + assert statistics['max'] == 98 + assert statistics['count'] == 10 + +def test_trend_data(trend_data): + assert len(trend_data) == 1 + # turn list containing 1 dict into a dict + trend_data = trend_data[0] + assert trend_data['min'] == trend_data['max'] == 0.086 + assert trend_data['count'] == 18 + # floating point values not exactly matching + assert 0.999 < trend_data['sum'] / (18 * 0.086) < 1.001 +def test_value_search(value_search): + assert len(value_search) == 2 + for record in value_search: + assert record['type'] == "HKQuantityTypeIdentifierStepCount" From 5fa274ef4bf8c2e7e256727713b9d4be92be89c3 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 16:17:30 +0200 Subject: [PATCH 22/77] linting i think + change textvalue case + all unit tests added --- README.md | 4 +- app/services/health/clickhouse.py | 2 +- app/services/health/duckdb_queries.py | 9 ++-- scripts/clickhouse_importer.py | 2 +- scripts/duckdb_importer.py | 21 ++++----- scripts/xml_exporter.py | 8 ++-- tests/import_tests.py | 4 -- tests/mcp.json | 2 +- tests/mcptest.py | 2 - tests/output.json | 2 +- tests/query_tests.py | 66 +++++++++++++++------------ 11 files changed, 62 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 0e8064d..c8590b3 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,8 @@ Follow these steps to set up Apple Health MCP Server in your environment. - Run `make duckdb` to create a parquet file with your exported XML data - If you want to connect to the file through http(s): - The only thing you need to do is change the .env path, e.g. `localhost:8080/applehealth.parquet` - - If you want an example on how to host the files locally, run `uv run tests/fileserver.py` - + - If you want an example on how to host the files locally, run `uv run tests/fileserver.py` + ### Configuration Files diff --git a/app/services/health/clickhouse.py b/app/services/health/clickhouse.py index 0a5bcd5..498cad4 100644 --- a/app/services/health/clickhouse.py +++ b/app/services/health/clickhouse.py @@ -49,7 +49,7 @@ def search_values_from_ch( date_to: str | None = None, ) -> dict[str, Any]: return ch.inquire(f""" - SELECT * FROM {ch.db_name}.{ch.table_name} WHERE textvalue = '{value}' + SELECT * FROM {ch.db_name}.{ch.table_name} WHERE textValue = '{value}' {f"AND type = '{record_type}'" if record_type else ""} {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 3681f1b..e15d63e 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -1,7 +1,6 @@ from typing import Any import duckdb -from duckdb import DuckDBPyRelation from fastmcp.server.dependencies import get_context from app.schemas.record import HealthRecordSearchParams, IntervalType, RecordType @@ -23,8 +22,7 @@ def get_health_summary_from_duckdb() -> list[dict[str, Any]]: ctx.error("Failed to connect to DuckDB") except RuntimeError: print("Failed to connect to DuckDB") - return [{'status_code': 400, 'error': 'failed to connect to DuckDB', - 'path': client.path}] + return [{"status_code": 400, "error": "failed to connect to DuckDB", "path": client.path}] return client.format_response(response) @@ -76,12 +74,13 @@ def search_values_from_duckdb( date_to: str | None = None, ) -> list[dict[str, Any]]: result = duckdb.sql(f""" - SELECT * FROM read_parquet('{client.path}') WHERE textvalue = '{value}' + SELECT * FROM read_parquet('{client.path}') WHERE textValue = '{value}' {f"AND type = '{record_type}'" if record_type else ""} {f"AND startDate >= '{date_from}'" if date_from else ""} {f"AND startDate <= '{date_to}'" if date_to else ""} """) return client.format_response(result) + if __name__ == "__main__": - print(get_health_summary_from_duckdb()) \ No newline at end of file + print(get_health_summary_from_duckdb()) diff --git a/scripts/clickhouse_importer.py b/scripts/clickhouse_importer.py index ee00c23..1805ae1 100644 --- a/scripts/clickhouse_importer.py +++ b/scripts/clickhouse_importer.py @@ -26,7 +26,7 @@ def create_table(self) -> None: creationDate DateTime, unit String, value Float32, - textvalue String, + textValue String, ) ENGINE = MergeTree ORDER BY startDate diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index da887e5..fe27201 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -1,9 +1,8 @@ import os from pathlib import Path -import pandas as pd -import polars as pl import duckdb +import polars as pl from app.services.duckdb_client import DuckDBClient from scripts.xml_exporter import XMLExporter @@ -15,15 +14,15 @@ def __init__(self): DuckDBClient.__init__(self) chunk_files = [] - + def write_to_file(self, index: int, df: pl.DataFrame) -> None: try: - if 'workoutActivityType' in df.columns: + if "workoutActivityType" in df.columns: chunk_file: Path = Path(f"workouts.chunk_{index}.parquet") print(f"processed {index * self.chunk_size} docs") df.write_parquet(chunk_file, compression="zstd", compression_level=1) self.chunk_files.append(chunk_file) - elif 'type' in df.columns: + elif "type" in df.columns: chunk_file: Path = Path(f"records.chunk_{index}.parquet") print(f"processed {index * self.chunk_size} docs") df.write_parquet(chunk_file, compression="zstd", compression_level=1) @@ -49,13 +48,13 @@ def exportxml(self) -> None: for chunk_file in self.chunk_files: df = pl.read_parquet(chunk_file) - if 'value' in df.columns: + if "value" in df.columns: df = df.select(self.RECORD_COLUMNS) record_chunk_dfs.append(df) - elif 'device' in df.columns: + elif "device" in df.columns: df = df.select(self.WORKOUT_COLUMNS) workout_chunk_dfs.append(df) - elif 'sum' in df.columns: + elif "sum" in df.columns: df = df.select(self.WORKOUT_STATS_COLUMNS) stat_chunk_dfs.append(df) @@ -76,11 +75,11 @@ def exportxml(self) -> None: raise RuntimeError(f"Failed to concatenate dataframes: {str(e)}") try: if record_df is not None: - record_df.write_parquet(f"{self.path / "records.parquet"}", compression="zstd") + record_df.write_parquet(f"{self.path / 'records.parquet'}", compression="zstd") if workout_df is not None: - workout_df.write_parquet(f"{self.path / "workouts.parquet"}", compression="zstd") + workout_df.write_parquet(f"{self.path / 'workouts.parquet'}", compression="zstd") if stat_df is not None: - stat_df.write_parquet(f"{self.path / "stats.parquet"}", compression="zstd") + stat_df.write_parquet(f"{self.path / 'stats.parquet'}", compression="zstd") except Exception as e: for f in self.chunk_files: os.remove(f) diff --git a/scripts/xml_exporter.py b/scripts/xml_exporter.py index 12e97c9..e5bfad6 100644 --- a/scripts/xml_exporter.py +++ b/scripts/xml_exporter.py @@ -3,8 +3,8 @@ from typing import Any, Generator from xml.etree import ElementTree as ET -from pandas import DataFrame import polars as pl +from pandas import DataFrame from app.config import settings @@ -31,7 +31,7 @@ def __init__(self): "creationDate", "unit", "value", - "textvalue", + "textValue", ) WORKOUT_COLUMNS: tuple[str, ...] = ( "type", @@ -55,7 +55,7 @@ def update_record(self, kind: str, document: dict[str, Any]) -> dict[str, Any]: Updates records to fill out columns without specified data: There are 9 columns that need to be filled out, and there are 4 columns that are optional and aren't filled out in every record - Additionally a textvalue field is added for querying text values + Additionally a textValue field is added for querying text values """ for field in self.DATE_FIELDS: if field in document: @@ -65,7 +65,7 @@ def update_record(self, kind: str, document: dict[str, Any]) -> dict[str, Any]: if len(document) != 9: document.update({k: v for k, v in self.DEFAULT_VALUES.items() if k not in document}) - document["textvalue"] = document["value"] + document["textValue"] = document["value"] try: document["value"] = float(document["value"]) diff --git a/tests/import_tests.py b/tests/import_tests.py index f21b633..e69de29 100644 --- a/tests/import_tests.py +++ b/tests/import_tests.py @@ -1,4 +0,0 @@ -import pytest - -from scripts import duckdb_importer - diff --git a/tests/mcp.json b/tests/mcp.json index 56fcdc0..f334ede 100644 --- a/tests/mcp.json +++ b/tests/mcp.json @@ -6,4 +6,4 @@ "note": "For Streamable HTTP connections, add this URL directly in your MCP Client" } } -} \ No newline at end of file +} diff --git a/tests/mcptest.py b/tests/mcptest.py index 7d121d0..a9e854a 100644 --- a/tests/mcptest.py +++ b/tests/mcptest.py @@ -9,7 +9,6 @@ from agents.model_settings import ModelSettings from dotenv import load_dotenv - load_dotenv() nest_asyncio.apply() @@ -48,5 +47,4 @@ async def main() -> None: print(result.final_output) - asyncio.run(main()) diff --git a/tests/output.json b/tests/output.json index c13c11a..fc46b8a 100644 --- a/tests/output.json +++ b/tests/output.json @@ -202,4 +202,4 @@ ] }, "isError": false -} \ No newline at end of file +} diff --git a/tests/query_tests.py b/tests/query_tests.py index 875da55..0b2f11b 100644 --- a/tests/query_tests.py +++ b/tests/query_tests.py @@ -6,88 +6,98 @@ path = Path(__file__).parent / "records.parquet" os.environ["DUCKDB_FILENAME"] = str(path) +from app.schemas.record import HealthRecordSearchParams from app.services.health.duckdb_queries import ( get_health_summary_from_duckdb, - search_health_records_from_duckdb, get_statistics_by_type_from_duckdb, get_trend_data_from_duckdb, - search_values_from_duckdb + search_health_records_from_duckdb, + search_values_from_duckdb, ) -from app.schemas.record import HealthRecordSearchParams @pytest.fixture def counts(): return { - 'HKQuantityTypeIdentifierBasalEnergyBurned': 18, - 'HKQuantityTypeIdentifierStepCount': 10, - 'HKQuantityTypeIdentifierHeartRate': 17, - 'HKQuantityTypeIdentifierBodyMassIndex': 8, - 'HKQuantityTypeIdentifierDietaryWater': 1, + "HKQuantityTypeIdentifierBasalEnergyBurned": 18, + "HKQuantityTypeIdentifierStepCount": 10, + "HKQuantityTypeIdentifierHeartRate": 17, + "HKQuantityTypeIdentifierBodyMassIndex": 8, + "HKQuantityTypeIdentifierDietaryWater": 1, } + @pytest.fixture def summary(): return get_health_summary_from_duckdb() + @pytest.fixture def records(): - return search_health_records_from_duckdb(HealthRecordSearchParams( - record_type="HKQuantityTypeIdentifierStepCount", - value_min = "65", - value_max = "90", - )) + return search_health_records_from_duckdb( + HealthRecordSearchParams( + record_type="HKQuantityTypeIdentifierStepCount", + value_min="65", + value_max="90", + ) + ) + @pytest.fixture def statistics(): return get_statistics_by_type_from_duckdb( - "HKQuantityTypeIdentifierStepCount" + "HKQuantityTypeIdentifierStepCount", ) + @pytest.fixture def trend_data(): return get_trend_data_from_duckdb( - record_type = "HKQuantityTypeIdentifierBasalEnergyBurned" + record_type="HKQuantityTypeIdentifierBasalEnergyBurned", ) + @pytest.fixture def value_search(): return search_values_from_duckdb( - record_type = "HKQuantityTypeIdentifierStepCount", - value = "13" + record_type="HKQuantityTypeIdentifierStepCount", + value="13", ) - def test_summary(summary, counts): assert len(summary) == 5 for record in summary: - assert record['count'] == counts[record['type']] + assert record["count"] == counts[record["type"]] + def test_records(records): assert len(records) == 3 for record in records: - assert 65 < record['value'] < 90 - assert record['type'] == "HKQuantityTypeIdentifierStepCount" + assert 65 < record["value"] < 90 + assert record["type"] == "HKQuantityTypeIdentifierStepCount" + def test_statistics(statistics): assert len(statistics) == 1 # turn list containing 1 dict into a dict statistics = statistics[0] - assert statistics['min'] == 3 - assert statistics['max'] == 98 - assert statistics['count'] == 10 + assert statistics["min"] == 3 + assert statistics["max"] == 98 + assert statistics["count"] == 10 + def test_trend_data(trend_data): assert len(trend_data) == 1 # turn list containing 1 dict into a dict trend_data = trend_data[0] - assert trend_data['min'] == trend_data['max'] == 0.086 - assert trend_data['count'] == 18 + assert trend_data["min"] == trend_data["max"] == 0.086 + assert trend_data["count"] == 18 # floating point values not exactly matching - assert 0.999 < trend_data['sum'] / (18 * 0.086) < 1.001 + assert 0.999 < trend_data["sum"] / (18 * 0.086) < 1.001 + def test_value_search(value_search): assert len(value_search) == 2 for record in value_search: - assert record['type'] == "HKQuantityTypeIdentifierStepCount" + assert record["type"] == "HKQuantityTypeIdentifierStepCount" From 504576b23c77e263718e8a42445b4c70ef6ba5e2 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 16:18:51 +0200 Subject: [PATCH 23/77] name fix --- scripts/duckdb_importer.py | 2 +- scripts/xml_exporter.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index fe27201..ba18d17 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -36,7 +36,7 @@ def write_to_file(self, index: int, df: pl.DataFrame) -> None: os.remove(file) raise RuntimeError(f"Failed to write chunk file to disk: {chunk_file}") - def exportxml(self) -> None: + def export_xml(self) -> None: for i, docs in enumerate(self.parse_xml(), 1): df: pl.DataFrame = pl.DataFrame(docs) self.write_to_file(i, df) diff --git a/scripts/xml_exporter.py b/scripts/xml_exporter.py index e5bfad6..d6f7855 100644 --- a/scripts/xml_exporter.py +++ b/scripts/xml_exporter.py @@ -11,7 +11,7 @@ class XMLExporter: def __init__(self): - self.xmlpath: Path = Path(settings.RAW_XML_PATH) + self.xml_path: Path = Path(settings.RAW_XML_PATH) self.chunk_size: int = settings.CHUNK_SIZE DATE_FIELDS: tuple[str, ...] = ("startDate", "endDate", "creationDate") @@ -91,7 +91,7 @@ def parse_xml(self) -> Generator[pl.DataFrame, Any, None]: workouts: list[dict[str, Any]] = [] workout_stats: list[dict[str, Any]] = [] - for event, elem in ET.iterparse(self.xmlpath, events=("start",)): + for event, elem in ET.iterparse(self.xml_path, events=("start",)): if elem.tag == "Record" and event == "start": if len(records) >= self.chunk_size: # yield pl.DataFrame(records) From 73c40ca147e9bedb14a3ef5543c0a867485f2916 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 24 Sep 2025 16:36:59 +0200 Subject: [PATCH 24/77] stupid linter --- pyproject.toml | 4 ++++ scripts/duckdb_importer.py | 6 +++--- tests/mcptest.py | 1 + tests/query_tests.py | 41 +++++++++++++++++++------------------- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0794c71..d0fa630 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ asyncio_default_fixture_loop_scope = "session" [tool.ruff] line-length = 100 target-version = "py313" +extend-exclude = ["tests/"] [tool.ruff.lint] select = [ @@ -73,5 +74,8 @@ ignore = [ "ANN401", # any-type ] +[tool.ty.src] +exclude = ["tests/"] + [project.scripts] start = "start:main" diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index ba18d17..68f9ff4 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -88,12 +88,12 @@ def export_xml(self) -> None: for f in self.chunk_files: os.remove(f) - def export_to_multiple(self): + def export_to_multiple(self) -> None: con = duckdb.connect("shitass.duckdb") for i, docs in enumerate(self.parse_xml(), 1): tables = docs.partition_by("type", as_dict=True) for key, table in tables.items(): - pddf = table.to_pandas() + pddf = table.to_pandas() # noqa con.execute(f""" CREATE TABLE IF NOT EXISTS {key[0]} AS SELECT * FROM pddf @@ -104,4 +104,4 @@ def export_to_multiple(self): if __name__ == "__main__": importer = ParquetImporter() - importer.exportxml() + importer.export_xml() diff --git a/tests/mcptest.py b/tests/mcptest.py index a9e854a..df392ea 100644 --- a/tests/mcptest.py +++ b/tests/mcptest.py @@ -1,3 +1,4 @@ +# ruff: noqa import asyncio import nest_asyncio diff --git a/tests/query_tests.py b/tests/query_tests.py index 0b2f11b..6af9df6 100644 --- a/tests/query_tests.py +++ b/tests/query_tests.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from typing import Any import pytest @@ -17,7 +18,7 @@ @pytest.fixture -def counts(): +def counts() -> dict[str, int]: return { "HKQuantityTypeIdentifierBasalEnergyBurned": 18, "HKQuantityTypeIdentifierStepCount": 10, @@ -28,76 +29,76 @@ def counts(): @pytest.fixture -def summary(): +def summary() -> list[dict[str, Any]]: return get_health_summary_from_duckdb() @pytest.fixture -def records(): +def records() -> list[dict[str, Any]]: return search_health_records_from_duckdb( HealthRecordSearchParams( record_type="HKQuantityTypeIdentifierStepCount", value_min="65", value_max="90", - ) + ), ) @pytest.fixture -def statistics(): +def statistics() -> list[dict[str, Any]]: return get_statistics_by_type_from_duckdb( "HKQuantityTypeIdentifierStepCount", ) @pytest.fixture -def trend_data(): +def trend_data() -> list[dict[str, Any]]: return get_trend_data_from_duckdb( record_type="HKQuantityTypeIdentifierBasalEnergyBurned", ) @pytest.fixture -def value_search(): +def value_search() -> list[dict[str, Any]]: return search_values_from_duckdb( record_type="HKQuantityTypeIdentifierStepCount", value="13", ) -def test_summary(summary, counts): +def test_summary(summary: list[dict[str, Any]], counts: dict[str, int]) -> None: assert len(summary) == 5 for record in summary: assert record["count"] == counts[record["type"]] -def test_records(records): +def test_records(records: list[dict[str, Any]]) -> None: assert len(records) == 3 for record in records: assert 65 < record["value"] < 90 assert record["type"] == "HKQuantityTypeIdentifierStepCount" -def test_statistics(statistics): +def test_statistics(statistics: list[dict[str, Any]] | dict[str, Any]) -> None: assert len(statistics) == 1 # turn list containing 1 dict into a dict - statistics = statistics[0] - assert statistics["min"] == 3 - assert statistics["max"] == 98 - assert statistics["count"] == 10 + stats: dict[str, Any] = statistics[0] + assert stats["min"] == 3 + assert stats["max"] == 98 + assert stats["count"] == 10 -def test_trend_data(trend_data): +def test_trend_data(trend_data: list[dict[str, Any]]) -> None: assert len(trend_data) == 1 # turn list containing 1 dict into a dict - trend_data = trend_data[0] - assert trend_data["min"] == trend_data["max"] == 0.086 - assert trend_data["count"] == 18 + data: dict[str, Any] = trend_data[0] + assert data["min"] == data["max"] == 0.086 + assert data["count"] == 18 # floating point values not exactly matching - assert 0.999 < trend_data["sum"] / (18 * 0.086) < 1.001 + assert 0.999 < data["sum"] / (18 * 0.086) < 1.001 -def test_value_search(value_search): +def test_value_search(value_search: list[dict[str, Any]]) -> None: assert len(value_search) == 2 for record in value_search: assert record["type"] == "HKQuantityTypeIdentifierStepCount" From 8f0d51526d81945f79f6a01cbd8be81a7fe1c243 Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 09:49:21 +0200 Subject: [PATCH 25/77] inspector workflow --- .github/workflows/inspector.yml | 14 ++++++++++++++ tests/mcp.json | 20 +++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/inspector.yml diff --git a/.github/workflows/inspector.yml b/.github/workflows/inspector.yml new file mode 100644 index 0000000..bd33ea9 --- /dev/null +++ b/.github/workflows/inspector.yml @@ -0,0 +1,14 @@ +name: learn-github-actions +run-name: ${{ github.actor }} is learning GitHub Actions +on: [push] +jobs: + check-bats-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npm install -g bats + - run: bats -v + - run: npx @modelcontextprotocol/inspector --cli --config tests/mcp.json --server apple-stdio --method tools/list \ No newline at end of file diff --git a/tests/mcp.json b/tests/mcp.json index f334ede..25e10db 100644 --- a/tests/mcp.json +++ b/tests/mcp.json @@ -3,7 +3,25 @@ "apple": { "type": "streamable-http", "url": "http://localhost:8000/mcp", - "note": "For Streamable HTTP connections, add this URL directly in your MCP Client" + "note": "For Streamable HTTP connections, add this URL directly in your MCP Client", + "env": { + "DUCKDB_FILENAME": "/mnt/c/Users/czajk/Desktop/apple/" + } + }, + "apple-stdio": { + "type": "stdio", + "command": "uv", + "args": [ + "run", + "--directory", + "../", + "fastmcp", + "run", + "app/main.py" + ], + "env": { + "DUCKDB_FILENAME": "/mnt/c/Users/czajk/Desktop/apple/" + } } } } From 08d5f88d87064db3726af849622fd2152adf3ef9 Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 09:50:40 +0200 Subject: [PATCH 26/77] add uv --- .github/workflows/inspector.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/inspector.yml b/.github/workflows/inspector.yml index bd33ea9..1bb8559 100644 --- a/.github/workflows/inspector.yml +++ b/.github/workflows/inspector.yml @@ -11,4 +11,6 @@ jobs: node-version: '20' - run: npm install -g bats - run: bats -v + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - run: npx @modelcontextprotocol/inspector --cli --config tests/mcp.json --server apple-stdio --method tools/list \ No newline at end of file From dc9a6d2bdfbd62eedfc4a5c1f73d81ff4fb8a402 Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 14:21:00 +0200 Subject: [PATCH 27/77] add e2e tests with llm judge and test workflow --- .github/workflows/inspector.yml | 3 +- .github/workflows/tests.yml | 25 + app/mcp/v1/mcp.py | 6 +- pyproject.toml | 1 + tests/agent.py | 83 +++ tests/e2e_tests.py | 80 +++ uv.lock | 999 +++++++++++++++++++++++++++++++- 7 files changed, 1190 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 tests/agent.py create mode 100644 tests/e2e_tests.py diff --git a/.github/workflows/inspector.yml b/.github/workflows/inspector.yml index 1bb8559..1190d1a 100644 --- a/.github/workflows/inspector.yml +++ b/.github/workflows/inspector.yml @@ -1,6 +1,7 @@ name: learn-github-actions -run-name: ${{ github.actor }} is learning GitHub Actions + on: [push] + jobs: check-bats-version: runs-on: ubuntu-latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1a370f9 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,25 @@ +name: CI + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Install dependencies + run: uv sync --group dev + - name: Run fileserver + run: uv run --directory tests/ fileserver.py + - name: Run mcp server + run: uv run fastmcp run -t http app/main.py + - name: Run tests + run: uv run --directory tests/ pytest e2e_tests.py + + diff --git a/app/mcp/v1/mcp.py b/app/mcp/v1/mcp.py index c698502..cc72222 100644 --- a/app/mcp/v1/mcp.py +++ b/app/mcp/v1/mcp.py @@ -5,6 +5,6 @@ mcp_router = FastMCP(name="Main MCP") mcp_router.mount(duckdb_reader.duckdb_reader_router) -mcp_router.mount(ch_reader.ch_reader_router) -mcp_router.mount(es_reader.es_reader_router) -mcp_router.mount(xml_reader.xml_reader_router) +# mcp_router.mount(ch_reader.ch_reader_router) +# mcp_router.mount(es_reader.es_reader_router) +# mcp_router.mount(xml_reader.xml_reader_router) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d0fa630..d8fda3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dev = [ "fastapi>=0.116.2", "nest-asyncio>=1.6.0", "openai-agents>=0.3.2", + "pydantic-ai>=1.0.10", "pytest>=8.4.1", "pytest-asyncio>=1.0.0", "pytest-cov>=6.2.1", diff --git a/tests/agent.py b/tests/agent.py new file mode 100644 index 0000000..da84203 --- /dev/null +++ b/tests/agent.py @@ -0,0 +1,83 @@ +import asyncio +import os + +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.openai import OpenAIProvider +from pydantic_ai import Agent +from pydantic_ai.tools import Tool +from pydantic_ai.mcp import MCPServerStreamableHTTP + +from dotenv import load_dotenv +load_dotenv() +from app.config import settings + +class AgentManager: + def __init__(self): + self.agent: Agent | None = None + self.mcp_client: MCPServerStreamableHTTP | None = None + self.tools: list[Tool] | None = None + self._initialized = False + + async def initialize(self, model: str = "gpt-4o", + system_prompt: str | None = None): + if self._initialized: + return + try: + self.mcp_client = MCPServerStreamableHTTP("http://localhost:8000/mcp") + except Exception as e: + self.mcp_client = None + raise ConnectionError("Could not connect to MCP server") from e + + if system_prompt is None: + system_prompt = "You are an AI assistant to help the user as best as you can. You can use the tools provided to you to help the user." + + self.agent = self._create_agent(model, system_prompt) + self._initialized = True + + def _create_agent(self, model: str, system_prompt: str) -> Agent: + model = OpenAIChatModel(model, provider=OpenAIProvider(api_key=os.getenv("api_key"))) + return Agent( + model=model, + deps_type=dict[str, str], + system_prompt=system_prompt, + toolsets=[self.mcp_client], + output_type=str, + ) + + async def handle_message(self, message: str) -> str: + if not self._initialized: + raise RuntimeError("Agent not initialized. Call initialize() first.") + + async with self.agent: + result = await self.agent.run(message) + return result.output + + def is_initialized(self) -> bool: + return self._initialized + + async def close(self): + """Close the MCP client""" + if self.mcp_client: + await self.mcp_client.close() + + +agent_manager = AgentManager() + + +async def main(): + await agent_manager.initialize() + + try: + while True: + user_input = input("Enter your message: ") + if user_input == "exit": + break + print("User: ", user_input) + response = await agent_manager.handle_message(user_input) + print("Agent: ", response) + finally: + await agent_manager.close() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/tests/e2e_tests.py b/tests/e2e_tests.py new file mode 100644 index 0000000..1c087c2 --- /dev/null +++ b/tests/e2e_tests.py @@ -0,0 +1,80 @@ +import asyncio +import pytest + +from pydantic_ai import capture_run_messages +from pydantic_ai.messages import ToolCallPart + +from tests.agent import AgentManager + + +agent_manager = AgentManager() +asyncio.run(agent_manager.initialize()) + + +async def tool_call_template(query: str, tool_name: str): + with capture_run_messages() as messages: + try: + await agent_manager.handle_message(query) + except ExceptionGroup: + pytest.fail("Failed to connect with MCP server", False) + finally: + print(messages) + assert len(messages) == 4 + resp = messages[1] + assert isinstance(resp.parts[0], ToolCallPart) + assert resp.parts[0].tool_name == tool_name + + +async def llm_opinion_template(query: str, expected: str): + with capture_run_messages() as messages: + try: + await agent_manager.handle_message(query) + assert len(messages) == 4 + output = messages[3] + resp = await agent_manager.handle_message(f""" + You are a judge that determines on a scale from 0-100% + how close two messages are to each other. You will receive + two inputs. Respond only with a percentage, e.g. "81", without % sign. + Consider things like missing or differing data, you can ignore + things like honorifics, your highest priority is data itself + + Input nr. 1: + {output.parts[0].content} + + Input nr. 2: + {expected} + + """) + percent = int(resp) + assert 75 < percent < 100 + + except ExceptionGroup: + pytest.fail("Failed to connect with MCP server", False) + finally: + print(messages) + + + +@pytest.mark.asyncio +async def test_summary(): + await tool_call_template("please give me a summary of my health from duckdb", + 'get_health_summary_duckdb') + +@pytest.mark.asyncio +async def test_judge(): + await llm_opinion_template("please give me a summary of my health from duckdb", + """Here's a summary of your health data from DuckDB: + - **Heart Rate**: 17 records - **Basal Energy Burned**: 17 records + - **Step Count**: 10 records - **Body Mass Index (BMI)**: 8 records + - **Dietary Water Intake**: 1 record""") + + + +# @pytest.mark.asyncio +# async def test_stats(): +# await query_template("please give me statistics of my step counts from duckdb") +# +# @pytest.mark.asyncio +# async def test_trend(): +# await query_template("please give me trend data for my heart rate") + diff --git a/uv.lock b/uv.lock index 3df2b2d..8ee27c9 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,73 @@ version = 1 revision = 2 requires-python = ">=3.13" +[[package]] +name = "ag-ui-protocol" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/d7/a8f8789b3b8b5f7263a902361468e8dfefd85ec63d1d5398579b9175d76d/ag_ui_protocol-0.1.9.tar.gz", hash = "sha256:94d75e3919ff75e0b608a7eed445062ea0e6f11cd33b3386a7649047e0c7abd3", size = 4988, upload-time = "2025-09-19T13:36:26.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/50/2bb71a2a9135f4d88706293773320d185789b592987c09f79e9bf2f4875f/ag_ui_protocol-0.1.9-py3-none-any.whl", hash = "sha256:44c1238b0576a3915b3a16e1b3855724e08e92ebc96b1ff29379fbd3bfbd400b", size = 7070, upload-time = "2025-09-19T13:36:25.791Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -11,6 +78,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anthropic" +version = "0.68.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/46/da44bf087ddaf3f7dbe4808c00c7cde466fe68c4fc9fbebdfc231f4ea205/anthropic-0.68.0.tar.gz", hash = "sha256:507e9b5f627d1b249128ff15b21855e718fa4ed8dabc787d0e68860a4b32a7a8", size = 471584, upload-time = "2025-09-17T15:20:19.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/32/2d7553184b05bdbec61dd600014a55b9028408aee6128b25cb6f20e3002c/anthropic-0.68.0-py3-none-any.whl", hash = "sha256:ac579ea5eca22a7165b1042e6af57c4bf556e51afae3ca80e24768d4756b78c0", size = 325199, upload-time = "2025-09-17T15:20:17.452Z" }, +] + [[package]] name = "anyio" version = "4.10.0" @@ -55,6 +141,7 @@ dev = [ { name = "fastapi" }, { name = "nest-asyncio" }, { name = "openai-agents" }, + { name = "pydantic-ai" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -88,11 +175,21 @@ dev = [ { name = "fastapi", specifier = ">=0.116.2" }, { name = "nest-asyncio", specifier = ">=1.6.0" }, { name = "openai-agents", specifier = ">=0.3.2" }, + { name = "pydantic-ai", specifier = ">=1.0.10" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=6.2.1" }, ] +[[package]] +name = "argcomplete" +version = "3.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403, upload-time = "2025-04-03T04:57:03.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" }, +] + [[package]] name = "attrs" version = "25.3.0" @@ -114,6 +211,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/2f/efa9d26dbb612b774990741fd8f13c7cf4cfd085b870e4a5af5c82eaf5f1/authlib-1.6.3-py2.py3-none-any.whl", hash = "sha256:7ea0f082edd95a03b7b72edac65ec7f8f68d703017d7e37573aee4fc603f2a48", size = 240105, upload-time = "2025-08-26T12:13:23.889Z" }, ] +[[package]] +name = "boto3" +version = "1.40.38" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/c7/1442380ad7e211089a3c94b758ffb01079eab0183700fba9d5be417b5cb4/boto3-1.40.38.tar.gz", hash = "sha256:932ebdd8dbf8ab5694d233df86d5d0950291e0b146c27cb46da8adb4f00f6ca4", size = 111559, upload-time = "2025-09-24T19:23:25.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a9/e7e5fe3fec60fb87bc9f8b3874c4c606e290a64b2ae8c157e08c3e69d755/boto3-1.40.38-py3-none-any.whl", hash = "sha256:fac337b4f0615e4d6ceee44686e662f51d8e57916ed2bc763468e3e8c611a658", size = 139345, upload-time = "2025-09-24T19:23:23.756Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.38" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/11/82a216e24f1af1ba5c3c358201fb9eba5e502242f504dd1f42eb18cbf2c5/botocore-1.40.38.tar.gz", hash = "sha256:18039009e1eca2bff12e576e8dd3c80cd9b312294f1469c831de03169582ad59", size = 14354395, upload-time = "2025-09-24T19:23:14.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f0/ca5a00dd8fe3768ecff54756457dd0c69ed8e1cd09d0f7c21599477b5d5b/botocore-1.40.38-py3-none-any.whl", hash = "sha256:7d60a7557db3a58f9394e7ecec1f6b87495ce947eb713f29d53aee83a6e9dc71", size = 14025193, upload-time = "2025-09-24T19:23:11.093Z" }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -235,6 +369,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] +[[package]] +name = "cohere" +version = "5.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/f5/4682a965449826044c853c82796805f8d3e9214471e2f120db3063116584/cohere-5.18.0.tar.gz", hash = "sha256:93a7753458a45cd30c796300182d22bb1889eadc510727e1de3d8342cb2bc0bf", size = 164340, upload-time = "2025-09-12T14:17:16.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/9b/3dc80542e60c711d57777b836a64345dda28f826c14fd64d9123278fcbfe/cohere-5.18.0-py3-none-any.whl", hash = "sha256:885e7be360206418db39425faa60dbcd7f38e39e7f84b824ee68442e6a436e93", size = 295384, upload-time = "2025-09-12T14:17:15.421Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -461,6 +615,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "eval-type-backport" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079, upload-time = "2024-12-21T20:09:46.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830, upload-time = "2024-12-21T20:09:44.175Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -470,6 +633,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + [[package]] name = "fastapi" version = "0.116.2" @@ -484,6 +656,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/e4/c543271a8018874b7f682bf6156863c416e1334b8ed3e51a69495c5d4360/fastapi-0.116.2-py3-none-any.whl", hash = "sha256:c3a7a8fb830b05f7e087d920e0d786ca1fc9892eb4e9a84b227be4c1bc7569db", size = 95670, upload-time = "2025-09-16T18:29:21.329Z" }, ] +[[package]] +name = "fastavro" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/ec/762dcf213e5b97ea1733b27d5a2798599a1fa51565b70a93690246029f84/fastavro-1.12.0.tar.gz", hash = "sha256:a67a87be149825d74006b57e52be068dfa24f3bfc6382543ec92cd72327fe152", size = 1025604, upload-time = "2025-07-31T15:16:42.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/c7/f18b73b39860d54eb724f881b8932882ba10c1d4905e491cd25d159a7e49/fastavro-1.12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dbe2b690d9caba7d888126cc1dd980a8fcf5ee73de41a104e3f15bb5e08c19c8", size = 936220, upload-time = "2025-07-31T15:17:21.994Z" }, + { url = "https://files.pythonhosted.org/packages/20/22/61ec800fda2a0f051a21b067e4005fd272070132d0a0566c5094e09b666c/fastavro-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07ff9e6c6e8739203ccced3205646fdac6141c2efc83f4dffabf5f7d0176646d", size = 3348450, upload-time = "2025-07-31T15:17:24.186Z" }, + { url = "https://files.pythonhosted.org/packages/ca/79/1f34618fb643b99e08853e8a204441ec11a24d3e1fce050e804e6ff5c5ae/fastavro-1.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a172655add31882cab4e1a96b7d49f419906b465b4c2165081db7b1db79852f", size = 3417238, upload-time = "2025-07-31T15:17:26.531Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0b/79611769eb15cc17992dc3699141feb0f75afd37b0cb964b4a08be45214e/fastavro-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:be20ce0331b70b35dca1a4c7808afeedf348dc517bd41602ed8fc9a1ac2247a9", size = 3252425, upload-time = "2025-07-31T15:17:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/86/1a/65e0999bcc4bbb38df32706b6ae6ce626d528228667a5e0af059a8b25bb2/fastavro-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a52906681384a18b99b47e5f9eab64b4744d6e6bc91056b7e28641c7b3c59d2b", size = 3385322, upload-time = "2025-07-31T15:17:31.232Z" }, + { url = "https://files.pythonhosted.org/packages/e9/49/c06ebc9e5144f7463c2bfcb900ca01f87db934caf131bccbffc5d0aaf7ec/fastavro-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:cf153531191bcfc445c21e05dd97232a634463aa717cf99fb2214a51b9886bff", size = 445586, upload-time = "2025-07-31T15:17:32.634Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c8/46ab37076dc0f86bb255791baf9b3c3a20f77603a86a40687edacff8c03d/fastavro-1.12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1928e88a760688e490118e1bedf0643b1f3727e5ba59c07ac64638dab81ae2a1", size = 1025933, upload-time = "2025-07-31T15:17:34.321Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7f/cb3e069dcc903034a6fe82182d92c75d981d86aee94bd028200a083696b3/fastavro-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd51b706a3ab3fe4af84a0b37f60d1bcd79295df18932494fc9f49db4ba2bab2", size = 3560435, upload-time = "2025-07-31T15:17:36.314Z" }, + { url = "https://files.pythonhosted.org/packages/d0/12/9478c28a2ac4fcc10ad9488dd3dcd5fac1ef550c3022c57840330e7cec4b/fastavro-1.12.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1148263931f6965e1942cf670f146148ca95b021ae7b7e1f98bf179f1c26cc58", size = 3453000, upload-time = "2025-07-31T15:17:38.875Z" }, + { url = "https://files.pythonhosted.org/packages/00/32/a5c8b3af9561c308c8c27da0be998b6237a47dbbdd8d5499f02731bd4073/fastavro-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4099e0f6fb8a55f59891c0aed6bfa90c4d20a774737e5282c74181b4703ea0cb", size = 3383233, upload-time = "2025-07-31T15:17:40.833Z" }, + { url = "https://files.pythonhosted.org/packages/42/a0/f6290f3f8059543faf3ef30efbbe9bf3e4389df881891136cd5fb1066b64/fastavro-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:10c586e9e3bab34307f8e3227a2988b6e8ac49bff8f7b56635cf4928a153f464", size = 3402032, upload-time = "2025-07-31T15:17:42.958Z" }, +] + [[package]] name = "fastmcp" version = "2.12.2" @@ -515,6 +706,116 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, ] +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/e0/bab50af11c2d75c9c4a2a26a5254573c0bd97cea152254401510950486fa/fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19", size = 304847, upload-time = "2025-09-02T19:10:49.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289, upload-time = "2025-09-02T19:10:47.708Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/f1/e9da3299662343f4757e7113bda469f9a3fcdec03a57e6f926ecae790620/genai_prices-0.0.27.tar.gz", hash = "sha256:e0ac07c9af75c6cd28c3feab5ed4dd7299e459975927145f1aa25317db3fb24d", size = 45451, upload-time = "2025-09-10T19:02:20.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/75/f2e11c7a357289934a26e45d60eb9892523e5e9b07ad886be7a8a35078b1/genai_prices-0.0.27-py3-none-any.whl", hash = "sha256:3f95bf72378ddfc88992755e33f1b208f15242697807d71ade5c1627caa56ce1", size = 48053, upload-time = "2025-09-10T19:02:19.416Z" }, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, +] + +[[package]] +name = "google-genai" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "google-auth" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/11/108ddd3aca8af6a9e2369e59b9646a3a4c64aefb39d154f6467ab8d79f34/google_genai-1.38.0.tar.gz", hash = "sha256:363272fc4f677d0be6a1aed7ebabe8adf45e1626a7011a7886a587e9464ca9ec", size = 244903, upload-time = "2025-09-16T23:25:42.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/6c/1de711bab3c118284904c3bedf870519e8c63a7a8e0905ac3833f1db9cbc/google_genai-1.38.0-py3-none-any.whl", hash = "sha256:95407425132d42b3fa11bc92b3f5cf61a0fbd8d9add1f0e89aac52c46fbba090", size = 245558, upload-time = "2025-09-16T23:25:41.141Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" }, +] + [[package]] name = "greenlet" version = "3.2.4" @@ -551,6 +852,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, ] +[[package]] +name = "groq" +version = "0.31.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/e9/f5d523ae8c78aa375addf44d1f64206271d43e6b42d4e5ce3dc76563a75b/groq-0.31.1.tar.gz", hash = "sha256:4d611e0100cb22732c43b53af37933a1b8a5c5a18fa96132fee14e6c15d737e6", size = 141400, upload-time = "2025-09-04T18:01:06.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/7d/877dbef7d72efacc657777b2e7897baa7cc7fcd0905f1b4a6423269e12a1/groq-0.31.1-py3-none-any.whl", hash = "sha256:536bd5dd6267dea5b3710e41094c0479748da2d155b9e073650e94b7fb2d71e8", size = 134903, upload-time = "2025-09-04T18:01:04.029Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -560,6 +878,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hf-xet" +version = "1.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/31/feeddfce1748c4a233ec1aa5b7396161c07ae1aa9b7bdbc9a72c3c7dd768/hf_xet-1.1.10.tar.gz", hash = "sha256:408aef343800a2102374a883f283ff29068055c111f003ff840733d3b715bb97", size = 487910, upload-time = "2025-09-12T20:10:27.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/a2/343e6d05de96908366bdc0081f2d8607d61200be2ac802769c4284cc65bd/hf_xet-1.1.10-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:686083aca1a6669bc85c21c0563551cbcdaa5cf7876a91f3d074a030b577231d", size = 2761466, upload-time = "2025-09-12T20:10:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/31/f9/6215f948ac8f17566ee27af6430ea72045e0418ce757260248b483f4183b/hf_xet-1.1.10-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71081925383b66b24eedff3013f8e6bbd41215c3338be4b94ba75fd75b21513b", size = 2623807, upload-time = "2025-09-12T20:10:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/86397573efefff941e100367bbda0b21496ffcdb34db7ab51912994c32a2/hf_xet-1.1.10-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6bceb6361c80c1cc42b5a7b4e3efd90e64630bcf11224dcac50ef30a47e435", size = 3186960, upload-time = "2025-09-12T20:10:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/01/a7/0b2e242b918cc30e1f91980f3c4b026ff2eedaf1e2ad96933bca164b2869/hf_xet-1.1.10-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eae7c1fc8a664e54753ffc235e11427ca61f4b0477d757cc4eb9ae374b69f09c", size = 3087167, upload-time = "2025-09-12T20:10:17.255Z" }, + { url = "https://files.pythonhosted.org/packages/4a/25/3e32ab61cc7145b11eee9d745988e2f0f4fafda81b25980eebf97d8cff15/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0a0005fd08f002180f7a12d4e13b22be277725bc23ed0529f8add5c7a6309c06", size = 3248612, upload-time = "2025-09-12T20:10:24.093Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3d/ab7109e607ed321afaa690f557a9ada6d6d164ec852fd6bf9979665dc3d6/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f", size = 3353360, upload-time = "2025-09-12T20:10:25.563Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl", hash = "sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045", size = 2804691, upload-time = "2025-09-12T20:10:28.433Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -590,11 +923,35 @@ wheels = [ [[package]] name = "httpx-sse" -version = "0.4.1" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.35.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/42/0e7be334a6851cd7d51cc11717cb95e89333ebf0064431c0255c56957526/huggingface_hub-0.35.1.tar.gz", hash = "sha256:3585b88c5169c64b7e4214d0e88163d4a709de6d1a502e0cd0459e9ee2c9c572", size = 461374, upload-time = "2025-09-23T13:43:47.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/60/4acf0c8a3925d9ff491dc08fe84d37e09cfca9c3b885e0db3d4dedb98cea/huggingface_hub-0.35.1-py3-none-any.whl", hash = "sha256:2f0e2709c711e3040e31d3e0418341f7092910f1462dd00350c4e97af47280a8", size = 563340, upload-time = "2025-09-23T13:43:45.343Z" }, +] + +[package.optional-dependencies] +inference = [ + { name = "aiohttp" }, ] [[package]] @@ -615,6 +972,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -624,6 +993,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "invoke" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835, upload-time = "2023-07-12T18:05:17.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274, upload-time = "2023-07-12T18:05:16.294Z" }, +] + [[package]] name = "isodate" version = "0.7.2" @@ -669,6 +1047,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/22/7ab7b4ec3a1c1f03aef376af11d23b05abcca3fb31fbca1e7557053b1ba2/jiter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406", size = 347102, upload-time = "2025-09-15T09:20:20.16Z" }, ] +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" @@ -737,6 +1124,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, ] +[[package]] +name = "logfire" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "executing" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/67/53bc8c72ae2deac94fe9dc51b9bade27c3f378469cf02336ae22558f2f41/logfire-4.10.0.tar.gz", hash = "sha256:5c1021dac8258d78d5fd08a336a22027df432c42ba70e96eef6cac7d8476a67c", size = 540375, upload-time = "2025-09-24T17:57:17.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/41/bbf361fd3a0576adbadd173492a22fcb1a194128df7609e728038a4a4f2d/logfire-4.10.0-py3-none-any.whl", hash = "sha256:54514b6253eea4c4e28f587b55508cdacbc75a423670bb5147fc2af70c16f5d3", size = 223648, upload-time = "2025-09-24T17:57:13.905Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "opentelemetry-instrumentation-httpx" }, +] + +[[package]] +name = "logfire-api" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/25/fb38c0e3f216ee72cda4d856147846f588a9ff9a863c2a981403916c3921/logfire_api-4.10.0.tar.gz", hash = "sha256:a9bf635a7c565c57f7c8145c0e7ac24ac4d34d0fb82774310d9b89d4c6968b6d", size = 55768, upload-time = "2025-09-24T17:57:18.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/e8/4355d4909eb1f07bba1ecf7a9b99be8bbc356db828e60b750e41dbb49dab/logfire_api-4.10.0-py3-none-any.whl", hash = "sha256:20819b2f3b43a53b66a500725553bdd52ed8c74f2147aa128c5ba5aa58668059", size = 92694, upload-time = "2025-09-24T17:57:15.686Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -808,6 +1227,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mistralai" +version = "1.9.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "httpx" }, + { name = "invoke" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/a3/1ae43c9db1fc612176d5d3418c12cd363852e954c5d12bf3a4477de2e4a6/mistralai-1.9.10.tar.gz", hash = "sha256:a95721276f035bf86c7fdc1373d7fb7d056d83510226f349426e0d522c0c0965", size = 205043, upload-time = "2025-09-02T07:44:38.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/40/646448b5ad66efec097471bd5ab25f5b08360e3f34aecbe5c4fcc6845c01/mistralai-1.9.10-py3-none-any.whl", hash = "sha256:cf0a2906e254bb4825209a26e1957e6e0bacbbe61875bd22128dc3d5d51a7b0a", size = 440538, upload-time = "2025-09-02T07:44:37.5Z" }, +] + [[package]] name = "more-itertools" version = "10.8.0" @@ -817,6 +1254,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] +[[package]] +name = "multidict" +version = "6.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" }, + { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload-time = "2025-08-11T12:07:23.636Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload-time = "2025-08-11T12:07:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload-time = "2025-08-11T12:07:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload-time = "2025-08-11T12:07:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload-time = "2025-08-11T12:07:29.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload-time = "2025-08-11T12:07:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload-time = "2025-08-11T12:07:32.205Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload-time = "2025-08-11T12:07:33.623Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload-time = "2025-08-11T12:07:34.958Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload-time = "2025-08-11T12:07:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload-time = "2025-08-11T12:07:39.735Z" }, + { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload-time = "2025-08-11T12:07:41.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload-time = "2025-08-11T12:07:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload-time = "2025-08-11T12:07:45.082Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload-time = "2025-08-11T12:07:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload-time = "2025-08-11T12:07:48.402Z" }, + { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload-time = "2025-08-11T12:07:49.679Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload-time = "2025-08-11T12:07:51.318Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload-time = "2025-08-11T12:07:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload-time = "2025-08-11T12:07:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload-time = "2025-08-11T12:07:55.914Z" }, + { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload-time = "2025-08-11T12:07:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload-time = "2025-08-11T12:07:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload-time = "2025-08-11T12:08:01.037Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload-time = "2025-08-11T12:08:02.96Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload-time = "2025-08-11T12:08:04.746Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload-time = "2025-08-11T12:08:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload-time = "2025-08-11T12:08:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload-time = "2025-08-11T12:08:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload-time = "2025-08-11T12:08:10.981Z" }, + { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload-time = "2025-08-11T12:08:12.439Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload-time = "2025-08-11T12:08:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload-time = "2025-08-11T12:08:15.173Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, +] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -826,6 +1308,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] +[[package]] +name = "nexus-rpc" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/66/540687556bd28cf1ec370cc6881456203dfddb9dab047b8979c6865b5984/nexus_rpc-1.1.0.tar.gz", hash = "sha256:d65ad6a2f54f14e53ebe39ee30555eaeb894102437125733fb13034a04a44553", size = 77383, upload-time = "2025-07-07T19:03:58.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2f/9e9d0dcaa4c6ffa22b7aa31069a8a264c753ff8027b36af602cce038c92f/nexus_rpc-1.1.0-py3-none-any.whl", hash = "sha256:d1b007af2aba186a27e736f8eaae39c03aed05b488084ff6c3d1785c9ba2ad38", size = 27743, upload-time = "2025-07-07T19:03:57.556Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -985,6 +1479,128 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923, upload-time = "2025-09-11T10:29:01.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/e3/6e320aeb24f951449e73867e53c55542bebbaf24faeee7623ef677d66736/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac", size = 17281, upload-time = "2025-09-11T10:29:04.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/e9/70d74a664d83976556cec395d6bfedd9b85ec1498b778367d5f93e373397/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef", size = 19576, upload-time = "2025-09-11T10:28:46.726Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/36/7c307d9be8ce4ee7beb86d7f1d31027f2a6a89228240405a858d6e4d64f9/opentelemetry_instrumentation-0.58b0.tar.gz", hash = "sha256:df640f3ac715a3e05af145c18f527f4422c6ab6c467e40bd24d2ad75a00cb705", size = 31549, upload-time = "2025-09-11T11:42:14.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/db/5ff1cd6c5ca1d12ecf1b73be16fbb2a8af2114ee46d4b0e6d4b23f4f4db7/opentelemetry_instrumentation-0.58b0-py3-none-any.whl", hash = "sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45", size = 33019, upload-time = "2025-09-11T11:41:00.624Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/21/ba3a0106795337716e5e324f58fd3c04f5967e330c0408d0d68d873454db/opentelemetry_instrumentation_httpx-0.58b0.tar.gz", hash = "sha256:3cd747e7785a06d06bd58875e8eb11595337c98c4341f4fe176ff1f734a90db7", size = 19887, upload-time = "2025-09-11T11:42:37.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/e7/6dc8ee4881889993fa4a7d3da225e5eded239c975b9831eff392abd5a5e4/opentelemetry_instrumentation_httpx-0.58b0-py3-none-any.whl", hash = "sha256:d3f5a36c7fed08c245f1b06d1efd91f624caf2bff679766df80981486daaccdb", size = 15197, upload-time = "2025-09-11T11:41:32.66Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.58b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/5f/02f31530faf50ef8a41ab34901c05cbbf8e9d76963ba2fb852b0b4065f4e/opentelemetry_util_http-0.58b0.tar.gz", hash = "sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89", size = 9411, upload-time = "2025-09-11T11:43:05.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652, upload-time = "2025-09-11T11:42:09.682Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1096,6 +1712,73 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +] + +[[package]] +name = "protobuf" +version = "5.29.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226, upload-time = "2025-05-28T23:51:59.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963, upload-time = "2025-05-28T23:51:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818, upload-time = "2025-05-28T23:51:44.297Z" }, + { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091, upload-time = "2025-05-28T23:51:45.907Z" }, + { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824, upload-time = "2025-05-28T23:51:47.545Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942, upload-time = "2025-05-28T23:51:49.11Z" }, + { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" }, +] + [[package]] name = "psycopg2-binary" version = "2.9.10" @@ -1137,6 +1820,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -1166,6 +1870,91 @@ email = [ { name = "email-validator" }, ] +[[package]] +name = "pydantic-ai" +version = "1.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b3/338c0c4a4d3479bae6067007e38c1cd315d571497aa2c55f5b7cb32202d2/pydantic_ai-1.0.10.tar.gz", hash = "sha256:b8218315d157e43b8a059ca74db2f515b97a2228e09a39855f26d211427e404c", size = 44299978, upload-time = "2025-09-20T00:16:16.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/1c/bcd1d5f883bb329b17a3229de3b4b89a9767646f3081499c5e9095af8bfa/pydantic_ai-1.0.10-py3-none-any.whl", hash = "sha256:c9300fbd988ec1e67211762edfbb19526f7fe5d978000ca65e1841bf74da78b7", size = 11680, upload-time = "2025-09-20T00:16:03.531Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "genai-prices" }, + { name = "griffe" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/a3/b24a2151c2e74c80b4745a2716cb81810214e1ff9508fdbb4a6542e28d37/pydantic_ai_slim-1.0.10.tar.gz", hash = "sha256:5922d9444718ad0d5d814e352844a93a28b9fcaa18d027a097760b0fb69a3d82", size = 251014, upload-time = "2025-09-20T00:16:22.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/87/c7d0ae2440f12260319c88ce509fe591b9a274ec2cd08eb2ce8b358baa4c/pydantic_ai_slim-1.0.10-py3-none-any.whl", hash = "sha256:f2c4fc7d653c4f6d75f4dd10e6ab4f1b5c139bf93664f1c0b6220c331c305091", size = 333279, upload-time = "2025-09-20T00:16:06.432Z" }, +] + +[package.optional-dependencies] +ag-ui = [ + { name = "ag-ui-protocol" }, + { name = "starlette" }, +] +anthropic = [ + { name = "anthropic" }, +] +bedrock = [ + { name = "boto3" }, +] +cli = [ + { name = "argcomplete" }, + { name = "prompt-toolkit" }, + { name = "pyperclip" }, + { name = "rich" }, +] +cohere = [ + { name = "cohere", marker = "sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals" }, +] +google = [ + { name = "google-genai" }, +] +groq = [ + { name = "groq" }, +] +huggingface = [ + { name = "huggingface-hub", extra = ["inference"] }, +] +logfire = [ + { name = "logfire", extra = ["httpx"] }, +] +mcp = [ + { name = "mcp" }, +] +mistral = [ + { name = "mistralai" }, +] +openai = [ + { name = "openai" }, +] +retries = [ + { name = "tenacity" }, +] +temporal = [ + { name = "temporalio" }, +] +vertexai = [ + { name = "google-auth" }, + { name = "requests" }, +] + [[package]] name = "pydantic-core" version = "2.33.2" @@ -1194,6 +1983,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, ] +[[package]] +name = "pydantic-evals" +version = "1.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/a6/2c3ced06c7164bf7bf7f4ec8ae232ed5adbaf05b309ca6755aa3b8b4e76e/pydantic_evals-1.0.10.tar.gz", hash = "sha256:341bfc105a3470373885ccbe70486064f783656c7c015c97152b2ba9351581e5", size = 45494, upload-time = "2025-09-20T00:16:23.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/ae/087d9a83dd7e91ad6c77e0d41d4ce25f24992cf0420412a19c045303568b/pydantic_evals-1.0.10-py3-none-any.whl", hash = "sha256:4146863594f851cdb606e7d9ddc445f298b53e40c9588d76a4794d792ba5b47a", size = 54608, upload-time = "2025-09-20T00:16:08.426Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/96/b778e8a7e4555670e4b6017441d054d26f3aceb534e89d6f25b7622a1b01/pydantic_graph-1.0.10.tar.gz", hash = "sha256:fc465ea8f29994098c43d44c69545d5917e2240d1e74b71d4ef1e06e86dea223", size = 21905, upload-time = "2025-09-20T00:16:24.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/ca/c9057a404002bad8c6b2d4a5187ee06ab03de1d6c72fc75d64df8f338980/pydantic_graph-1.0.10-py3-none-any.whl", hash = "sha256:8b47db36228303e4b91a1311eba068750057c0aafcbf476e14b600a80d4627d5", size = 27548, upload-time = "2025-09-20T00:16:10.933Z" }, +] + [[package]] name = "pydantic-settings" version = "2.10.1" @@ -1466,6 +2287,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, ] +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + [[package]] name = "ruff" version = "0.12.12" @@ -1492,6 +2325,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, ] +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1555,6 +2400,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" }, ] +[[package]] +name = "temporalio" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nexus-rpc" }, + { name = "protobuf" }, + { name = "types-protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/a7/622047cb731a104e455687793d724ed143925e9ea14b522ad5ce224e8d7f/temporalio-1.17.0.tar.gz", hash = "sha256:1ac8f1ade36fafe7110b979b6a16d89203e1f4fb9c874f2fe3b5d83c17b13244", size = 1734067, upload-time = "2025-09-03T01:27:05.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/9a/f6fd68e60afc67c402c0676c12baba3aa04d522c74f4123ed31b544d4159/temporalio-1.17.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7a86948c74a872b7f5ecb51c5d7e8013fdda4d6a220fe92185629342e94393e7", size = 12905249, upload-time = "2025-09-03T01:26:51.93Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7e/54cffb6a0ef4853f51bcefe5a74508940bad72a4442e50b3d52379a941c3/temporalio-1.17.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:00b34a986012a355bdadf0e7eb9e57e176f2e0b1d69ea4be9eb73c21672e7fd0", size = 12539749, upload-time = "2025-09-03T01:26:54.854Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/e4c829eb31bdb5eb14411ce7765b4ad8087794231110ff6188497859f0e6/temporalio-1.17.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a84e52727e287e13777d86fa0bbda11ba6523f75a616b811cc9d799b37b98c", size = 12969855, upload-time = "2025-09-03T01:26:57.464Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/fef412e10408e35888815ac06c0c777cff1faa76157d861878d23a17edf0/temporalio-1.17.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:617f37edce3db97cc7d2ff81c145a1b92c100f6e0e42207739271d10c2eea38e", size = 13165153, upload-time = "2025-09-03T01:27:00.285Z" }, + { url = "https://files.pythonhosted.org/packages/58/2d/01d164b78ea414f1e2554cd9959ffcf95f0c91a6d595f03128a70e433f57/temporalio-1.17.0-cp39-abi3-win_amd64.whl", hash = "sha256:f2724220fda1fd5948d917350ac25069c62624f46e53d4d6c6171baa75681145", size = 13178439, upload-time = "2025-09-03T01:27:02.855Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + [[package]] name = "testcontainers" version = "4.13.0" @@ -1596,6 +2469,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/ac/03bbe688c090b5e16b507b4e36d7c4e5d95e2a0861dd77922801088edfb1/testcontainers_postgres-0.0.1rc1-py3-none-any.whl", hash = "sha256:1bd0afcff2c236c08ffbf3e4926e713d8c58e20df82c31e62fb9cca70582fd5a", size = 2906, upload-time = "2023-01-06T16:37:45.675Z" }, ] +[[package]] +name = "tokenizers" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, + { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, + { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, +] + [[package]] name = "tqdm" version = "4.67.1" @@ -1633,6 +2531,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/36/5a3a70c5d497d3332f9e63cabc9c6f13484783b832fecc393f4f1c0c4aa8/ty-0.0.1a20-py3-none-win_arm64.whl", hash = "sha256:d8ac1c5a14cda5fad1a8b53959d9a5d979fe16ce1cc2785ea8676fed143ac85f", size = 8269906, upload-time = "2025-09-03T12:35:45.045Z" }, ] +[[package]] +name = "types-protobuf" +version = "6.32.1.20250918" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/5a/bd06c2dbb77ebd4ea764473c9c4c014c7ba94432192cb965a274f8544b9d/types_protobuf-6.32.1.20250918.tar.gz", hash = "sha256:44ce0ae98475909ca72379946ab61a4435eec2a41090821e713c17e8faf5b88f", size = 63780, upload-time = "2025-09-18T02:50:39.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/5a/8d93d4f4af5dc3dd62aa4f020deae746b34b1d94fb5bee1f776c6b7e9d6c/types_protobuf-6.32.1.20250918-py3-none-any.whl", hash = "sha256:22ba6133d142d11cc34d3788ad6dead2732368ebb0406eaa7790ea6ae46c8d0b", size = 77885, upload-time = "2025-09-18T02:50:38.028Z" }, +] + [[package]] name = "types-requests" version = "2.32.4.20250913" @@ -1711,6 +2618,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, ] +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + [[package]] name = "werkzeug" version = "3.1.1" @@ -1761,3 +2697,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From befeebbbb7282698987aa35567f1197fa051be0b Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 14:23:22 +0200 Subject: [PATCH 28/77] workflow tweak --- .github/workflows/tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1a370f9..de0ab1e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,9 +16,10 @@ jobs: - name: Install dependencies run: uv sync --group dev - name: Run fileserver - run: uv run --directory tests/ fileserver.py + run: uv run --directory tests/ fileserver.py & - name: Run mcp server - run: uv run fastmcp run -t http app/main.py + run: uv run fastmcp run -t http app/main.py & + - run: sleep 5 - name: Run tests run: uv run --directory tests/ pytest e2e_tests.py From 16b525058082128f0d10cd43311c48d7844d3c94 Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 14:29:00 +0200 Subject: [PATCH 29/77] hehehe --- .github/workflows/tests.yml | 3 + tests/output.json | 205 ------------------------------------ tests/parquet_example | Bin 0 -> 5750 bytes 3 files changed, 3 insertions(+), 205 deletions(-) delete mode 100644 tests/output.json create mode 100644 tests/parquet_example diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index de0ab1e..df731c3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,6 +2,9 @@ name: CI on: [push] +env: + DUCKDB_FILENAME: parquet_example + jobs: test: runs-on: ubuntu-latest diff --git a/tests/output.json b/tests/output.json deleted file mode 100644 index fc46b8a..0000000 --- a/tests/output.json +++ /dev/null @@ -1,205 +0,0 @@ -{ - "content": [ - { - "type": "text", - "text": "[{\"type\":\"HKQuantityTypeIdentifierActiveEnergyBurned\",\"count\":853486},{\"type\":\"HKQuantityTypeIdentifierHeartRate\",\"count\":411634},{\"type\":\"HKQuantityTypeIdentifierBasalEnergyBurned\",\"count\":297838},{\"type\":\"HKQuantityTypeIdentifierDistanceWalkingRunning\",\"count\":291024},{\"type\":\"HKQuantityTypeIdentifierStepCount\",\"count\":249130},{\"type\":\"HKQuantityTypeIdentifierRunningSpeed\",\"count\":80848},{\"type\":\"HKQuantityTypeIdentifierAppleExerciseTime\",\"count\":73715},{\"type\":\"HKQuantityTypeIdentifierRunningPower\",\"count\":68738},{\"type\":\"HKQuantityTypeIdentifierAppleStandTime\",\"count\":45524},{\"type\":\"HKQuantityTypeIdentifierFlightsClimbed\",\"count\":44125},{\"type\":\"HKQuantityTypeIdentifierWalkingStepLength\",\"count\":41021},{\"type\":\"HKQuantityTypeIdentifierWalkingSpeed\",\"count\":41016},{\"type\":\"HKQuantityTypeIdentifierEnvironmentalAudioExposure\",\"count\":40515},{\"type\":\"HKQuantityTypeIdentifierRunningVerticalOscillation\",\"count\":33382},{\"type\":\"HKQuantityTypeIdentifierRunningStrideLength\",\"count\":31271},{\"type\":\"HKQuantityTypeIdentifierWalkingDoubleSupportPercentage\",\"count\":30273},{\"type\":\"HKQuantityTypeIdentifierRunningGroundContactTime\",\"count\":28981},{\"type\":\"HKCategoryTypeIdentifierAppleStandHour\",\"count\":24515},{\"type\":\"HKQuantityTypeIdentifierRespiratoryRate\",\"count\":23939},{\"type\":\"HKQuantityTypeIdentifierHeadphoneAudioExposure\",\"count\":18757},{\"type\":\"HKQuantityTypeIdentifierStairDescentSpeed\",\"count\":14880},{\"type\":\"HKQuantityTypeIdentifierOxygenSaturation\",\"count\":13319},{\"type\":\"HKQuantityTypeIdentifierWalkingAsymmetryPercentage\",\"count\":9659},{\"type\":\"HKQuantityTypeIdentifierStairAscentSpeed\",\"count\":9527},{\"type\":\"HKQuantityTypeIdentifierHeartRateVariabilitySDNN\",\"count\":6904},{\"type\":\"HKQuantityTypeIdentifierBodyMass\",\"count\":3747},{\"type\":\"HKQuantityTypeIdentifierBodyMassIndex\",\"count\":3344},{\"type\":\"HKQuantityTypeIdentifierRestingHeartRate\",\"count\":894},{\"type\":\"HKQuantityTypeIdentifierWalkingHeartRateAverage\",\"count\":877},{\"type\":\"HKQuantityTypeIdentifierVO2Max\",\"count\":739},{\"type\":\"HKQuantityTypeIdentifierEnvironmentalSoundReduction\",\"count\":632},{\"type\":\"HKCategoryTypeIdentifierAudioExposureEvent\",\"count\":250},{\"type\":\"HKQuantityTypeIdentifierSixMinuteWalkTestDistance\",\"count\":104},{\"type\":\"HKCategoryTypeIdentifierHandwashingEvent\",\"count\":86},{\"type\":\"HKCategoryTypeIdentifierMindfulSession\",\"count\":84},{\"type\":\"HKQuantityTypeIdentifierAppleWalkingSteadiness\",\"count\":73},{\"type\":\"HKQuantityTypeIdentifierHeartRateRecoveryOneMinute\",\"count\":67},{\"type\":\"HKQuantityTypeIdentifierDistanceCycling\",\"count\":49},{\"type\":\"HKQuantityTypeIdentifierDistanceSwimming\",\"count\":39},{\"type\":\"HKQuantityTypeIdentifierSwimmingStrokeCount\",\"count\":39},{\"type\":\"HKCategoryTypeIdentifierHeadphoneAudioExposureEvent\",\"count\":13},{\"type\":\"HKQuantityTypeIdentifierDietaryEnergyConsumed\",\"count\":6},{\"type\":\"HKCategoryTypeIdentifierHighHeartRateEvent\",\"count\":5},{\"type\":\"HKCategoryTypeIdentifierLowHeartRateEvent\",\"count\":4},{\"type\":\"HKQuantityTypeIdentifierHeight\",\"count\":2},{\"type\":\"HKQuantityTypeIdentifierDietaryWater\",\"count\":1},{\"type\":\"HKDataTypeSleepDurationGoal\",\"count\":1},{\"type\":\"HKQuantityTypeIdentifierNumberOfTimesFallen\",\"count\":1}]" - } - ], - "structuredContent": { - "result": [ - { - "type": "HKQuantityTypeIdentifierActiveEnergyBurned", - "count": 853486 - }, - { - "type": "HKQuantityTypeIdentifierHeartRate", - "count": 411634 - }, - { - "type": "HKQuantityTypeIdentifierBasalEnergyBurned", - "count": 297838 - }, - { - "type": "HKQuantityTypeIdentifierDistanceWalkingRunning", - "count": 291024 - }, - { - "type": "HKQuantityTypeIdentifierStepCount", - "count": 249130 - }, - { - "type": "HKQuantityTypeIdentifierRunningSpeed", - "count": 80848 - }, - { - "type": "HKQuantityTypeIdentifierAppleExerciseTime", - "count": 73715 - }, - { - "type": "HKQuantityTypeIdentifierRunningPower", - "count": 68738 - }, - { - "type": "HKQuantityTypeIdentifierAppleStandTime", - "count": 45524 - }, - { - "type": "HKQuantityTypeIdentifierFlightsClimbed", - "count": 44125 - }, - { - "type": "HKQuantityTypeIdentifierWalkingStepLength", - "count": 41021 - }, - { - "type": "HKQuantityTypeIdentifierWalkingSpeed", - "count": 41016 - }, - { - "type": "HKQuantityTypeIdentifierEnvironmentalAudioExposure", - "count": 40515 - }, - { - "type": "HKQuantityTypeIdentifierRunningVerticalOscillation", - "count": 33382 - }, - { - "type": "HKQuantityTypeIdentifierRunningStrideLength", - "count": 31271 - }, - { - "type": "HKQuantityTypeIdentifierWalkingDoubleSupportPercentage", - "count": 30273 - }, - { - "type": "HKQuantityTypeIdentifierRunningGroundContactTime", - "count": 28981 - }, - { - "type": "HKCategoryTypeIdentifierAppleStandHour", - "count": 24515 - }, - { - "type": "HKQuantityTypeIdentifierRespiratoryRate", - "count": 23939 - }, - { - "type": "HKQuantityTypeIdentifierHeadphoneAudioExposure", - "count": 18757 - }, - { - "type": "HKQuantityTypeIdentifierStairDescentSpeed", - "count": 14880 - }, - { - "type": "HKQuantityTypeIdentifierOxygenSaturation", - "count": 13319 - }, - { - "type": "HKQuantityTypeIdentifierWalkingAsymmetryPercentage", - "count": 9659 - }, - { - "type": "HKQuantityTypeIdentifierStairAscentSpeed", - "count": 9527 - }, - { - "type": "HKQuantityTypeIdentifierHeartRateVariabilitySDNN", - "count": 6904 - }, - { - "type": "HKQuantityTypeIdentifierBodyMass", - "count": 3747 - }, - { - "type": "HKQuantityTypeIdentifierBodyMassIndex", - "count": 3344 - }, - { - "type": "HKQuantityTypeIdentifierRestingHeartRate", - "count": 894 - }, - { - "type": "HKQuantityTypeIdentifierWalkingHeartRateAverage", - "count": 877 - }, - { - "type": "HKQuantityTypeIdentifierVO2Max", - "count": 739 - }, - { - "type": "HKQuantityTypeIdentifierEnvironmentalSoundReduction", - "count": 632 - }, - { - "type": "HKCategoryTypeIdentifierAudioExposureEvent", - "count": 250 - }, - { - "type": "HKQuantityTypeIdentifierSixMinuteWalkTestDistance", - "count": 104 - }, - { - "type": "HKCategoryTypeIdentifierHandwashingEvent", - "count": 86 - }, - { - "type": "HKCategoryTypeIdentifierMindfulSession", - "count": 84 - }, - { - "type": "HKQuantityTypeIdentifierAppleWalkingSteadiness", - "count": 73 - }, - { - "type": "HKQuantityTypeIdentifierHeartRateRecoveryOneMinute", - "count": 67 - }, - { - "type": "HKQuantityTypeIdentifierDistanceCycling", - "count": 49 - }, - { - "type": "HKQuantityTypeIdentifierDistanceSwimming", - "count": 39 - }, - { - "type": "HKQuantityTypeIdentifierSwimmingStrokeCount", - "count": 39 - }, - { - "type": "HKCategoryTypeIdentifierHeadphoneAudioExposureEvent", - "count": 13 - }, - { - "type": "HKQuantityTypeIdentifierDietaryEnergyConsumed", - "count": 6 - }, - { - "type": "HKCategoryTypeIdentifierHighHeartRateEvent", - "count": 5 - }, - { - "type": "HKCategoryTypeIdentifierLowHeartRateEvent", - "count": 4 - }, - { - "type": "HKQuantityTypeIdentifierHeight", - "count": 2 - }, - { - "type": "HKQuantityTypeIdentifierDietaryWater", - "count": 1 - }, - { - "type": "HKDataTypeSleepDurationGoal", - "count": 1 - }, - { - "type": "HKQuantityTypeIdentifierNumberOfTimesFallen", - "count": 1 - } - ] - }, - "isError": false -} diff --git a/tests/parquet_example b/tests/parquet_example new file mode 100644 index 0000000000000000000000000000000000000000..2594a83b3c3be7665d1ce038850678e1f62171e0 GIT binary patch literal 5750 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 Date: Thu, 25 Sep 2025 14:37:23 +0200 Subject: [PATCH 30/77] commit --- tests/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/agent.py b/tests/agent.py index da84203..53cc38c 100644 --- a/tests/agent.py +++ b/tests/agent.py @@ -35,7 +35,7 @@ async def initialize(self, model: str = "gpt-4o", self._initialized = True def _create_agent(self, model: str, system_prompt: str) -> Agent: - model = OpenAIChatModel(model, provider=OpenAIProvider(api_key=os.getenv("api_key"))) + model = OpenAIChatModel(model, provider=OpenAIProvider(api_key=os.getenv("openai_api_key"))) return Agent( model=model, deps_type=dict[str, str], From b337cbb3b69318c037eacfc45b75a9ed53c09dc7 Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 14:43:25 +0200 Subject: [PATCH 31/77] ennvar --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index df731c3..ca9e2b8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,7 @@ on: [push] env: DUCKDB_FILENAME: parquet_example + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} jobs: test: From 89e070beaf522cdc8769834e4806f3f277ee846c Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 15:25:52 +0200 Subject: [PATCH 32/77] added inspector test --- .github/workflows/tests.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ca9e2b8..8519081 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: CI +name: tests on: [push] @@ -26,5 +26,12 @@ jobs: - run: sleep 5 - name: Run tests run: uv run --directory tests/ pytest e2e_tests.py - + inspector: + runs-on: ubuntu-latest + steps: + - name: Run mcp server + run: uv run fastmcp run -t http app/main.py & + - name: Run inspector + run: npx @modelcontextprotocol/inspector --cli \ + http://localhost:8000/mcp --method tools/list From c26fe15aebeca4d4cb9ebe7b42b09452fda0283e Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 15:28:21 +0200 Subject: [PATCH 33/77] node uv --- .github/workflows/inspector.yml | 2 +- .github/workflows/tests.yml | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/inspector.yml b/.github/workflows/inspector.yml index 1190d1a..e760d9f 100644 --- a/.github/workflows/inspector.yml +++ b/.github/workflows/inspector.yml @@ -1,4 +1,4 @@ -name: learn-github-actions +name: kijjhjvjm-github-actions on: [push] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8519081..8052542 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,9 +29,14 @@ jobs: inspector: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Run mcp server run: uv run fastmcp run -t http app/main.py & - name: Run inspector - run: npx @modelcontextprotocol/inspector --cli \ - http://localhost:8000/mcp --method tools/list + run: npx @modelcontextprotocol/inspector --cli \ http://localhost:8000/mcp --method tools/list From 817e67177141e772800a2a2f7f994c6ab89ac62b Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 15:30:00 +0200 Subject: [PATCH 34/77] dev deps --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8052542..ab09180 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,6 +35,8 @@ jobs: node-version: '20' - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Install dependencies + run: uv sync --group dev - name: Run mcp server run: uv run fastmcp run -t http app/main.py & - name: Run inspector From 9e068f4d81edbc923eb23ef96089f6817904a274 Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 15:33:11 +0200 Subject: [PATCH 35/77] test file paht --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ab09180..ece5978 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: tests on: [push] env: - DUCKDB_FILENAME: parquet_example + DUCKDB_FILENAME: tests/parquet_example OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} jobs: From 6dcc5a80365c904291913cb2a8e3e26f2fe278fd Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 25 Sep 2025 15:34:18 +0200 Subject: [PATCH 36/77] mcp path --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ece5978..027862a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,5 +40,5 @@ jobs: - name: Run mcp server run: uv run fastmcp run -t http app/main.py & - name: Run inspector - run: npx @modelcontextprotocol/inspector --cli \ http://localhost:8000/mcp --method tools/list + run: npx @modelcontextprotocol/inspector --cli \ http://127.0.0.1:8000/mcp --method tools/list From 4d33a703e9371cd51baa1e07051142ab7dd65bea Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 26 Sep 2025 14:00:11 +0200 Subject: [PATCH 37/77] opik added --- tests/agent.py | 2 + tests/e2e_tests.py | 3 +- tests/mcp-test/tools.py | 0 tests/opik/tool_calls.py | 151 +++++++++++++++++++++++++++++++++++++++ tests/tools_test.py | 22 ++++++ 5 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 tests/mcp-test/tools.py create mode 100644 tests/opik/tool_calls.py create mode 100644 tests/tools_test.py diff --git a/tests/agent.py b/tests/agent.py index 53cc38c..9fafffe 100644 --- a/tests/agent.py +++ b/tests/agent.py @@ -10,6 +10,7 @@ from dotenv import load_dotenv load_dotenv() from app.config import settings +import opik class AgentManager: def __init__(self): @@ -44,6 +45,7 @@ def _create_agent(self, model: str, system_prompt: str) -> Agent: output_type=str, ) + @opik.track async def handle_message(self, message: str) -> str: if not self._initialized: raise RuntimeError("Agent not initialized. Call initialize() first.") diff --git a/tests/e2e_tests.py b/tests/e2e_tests.py index 1c087c2..0f1196b 100644 --- a/tests/e2e_tests.py +++ b/tests/e2e_tests.py @@ -46,7 +46,7 @@ async def llm_opinion_template(query: str, expected: str): """) percent = int(resp) - assert 75 < percent < 100 + assert 75 <= percent <= 100 except ExceptionGroup: pytest.fail("Failed to connect with MCP server", False) @@ -78,3 +78,4 @@ async def test_judge(): # async def test_trend(): # await query_template("please give me trend data for my heart rate") + diff --git a/tests/mcp-test/tools.py b/tests/mcp-test/tools.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py new file mode 100644 index 0000000..6d12b74 --- /dev/null +++ b/tests/opik/tool_calls.py @@ -0,0 +1,151 @@ +import asyncio + +import opik +from opik.evaluation import evaluate +from opik.evaluation.metrics import ( + base_metric, + score_result, + Hallucination, + LevenshteinRatio, + AnswerRelevance +) +import nest_asyncio + +from tests.agent import AgentManager + +nest_asyncio.apply() + +agent_manager = AgentManager() +asyncio.run(agent_manager.initialize()) + + +class ToolSelectionQuality(base_metric.BaseMetric): + def __init__(self, name: str = "tool_selection_quality"): + # super().__init__(name) + self.name = name + + def score(self, tool_calls, expected_tool_calls, **kwargs): + try: + actual_tool = tool_calls[0]["function_name"] + expected_tool = expected_tool_calls[0]["function_name"] + if actual_tool == expected_tool: + return score_result.ScoreResult( + name=self.name, + value=1, + reason=f"Correct tool selected: {actual_tool}" + ) + else: + return score_result.ScoreResult( + name=self.name, + value=0, + reason=f"Wrong tool. Expected {expected_tool}, got {actual_tool}" + ) + except Exception as e: + return score_result.ScoreResult( + name=self.name, + value=0, + reason=f"Scoring error: {e}" + ) + + +def evaluation_task(dataset_item): + try: + user_message_content = dataset_item["input"] + expected_tool = dataset_item.get("tool_call", "") + reference = dataset_item.get("expected_output", "") + # This is where you call your agent with the input message and get the real execution results. + resp = (agent_manager.agent.run_sync(user_message_content)) + result = resp.new_messages() + tool_calls = [{"function_name": result[1].parts[0].tool_name, + "function_parameters": {}}] + return { + "input": user_message_content, + "output": resp.output, + "reference": reference, + "tool_calls": tool_calls, + "expected_tool_calls": [{"function_name": expected_tool, "function_parameters": {}}] + } + except Exception as e: + return { + "input": dataset_item.get("input", {}), + "output": "Error processing input.", + "reference": dataset_item.get("expected_output", ""), + "tool_calls": [], + "expected_tool_calls": [{"function_name": "unknown", "function_parameters": {}}], + "error": str(e) + } + + +metrics = [ToolSelectionQuality()] + +client = opik.Opik() + +dataset = client.get_dataset(name="tool_calls") + +dataset.insert([ + # { + # "input": "search for step count records between 65 and 90 steps", + # "tool_call": "search_health_records_duckdb" + # }, + { + "input": "can you search for step records with exactly 13 steps", + "tool_call": "search_values_duckdb" + } +]) + +judge_dataset = client.get_or_create_dataset(name="output_checks") + +judge_dataset.insert([ + { + "input": "search for step count records between 65 and 90 steps", + "expected_output": """ + I found some step count records with values between 65 and 90 steps: + 1. **76 Steps** - **Source:** Rob’s Apple Watch - **Device:** + Apple Watch, model Watch6,1 - **Date:** October 25, 2020 + - **Start Time:** 00:05:13 - **End Time:** 00:06:14 + 2. **87 Steps** - **Source:** Rob’s Apple Watch + - **Device:** Apple Watch, model Watch6,1 + - **Date:** October 25, 2020 - **Start Time:** 00:06:14 + - **End Time:** 00:07:15 3. **66 Steps** - **Source:** + Rob’s Apple Watch - **Device:** Apple Watch, model Watch6,1 + - **Date:** October 25, 2020 - **Start Time:** 00:11:35 - + **End Time:** 00:12:37 If you need more details or additional + records, feel free to ask! + """ + }, + { + "input": "can you search for step records with exactly 13 steps", + "expected_output": """ + I found two records of step count with exactly 13 steps. + Here are the details: 1. **Record 1:** - **Source Name:** + Rob’s Apple Watch - **Source Version:** 7.0.2 - **Device:** + Apple Watch, manufacturer: Apple Inc., model: Watch, hardware: + Watch6,1, software: 7.0.2 - **Start Date:** 2020-10-24 + 23:54:51 +02:00 - **End Date:** 2020-10-24 23:55:30 +02:00 + - **Creation Date:** 2020-10-25 00:04:01 +02:00 - **Unit:** + Count - **Value:** 13 2. **Record 2:** - **Source Name:** + Rob’s Apple Watch - **Source Version:** 7.0.2 - **Device:** Apple + Watch, manufacturer: Apple Inc., model: Watch, hardware: Watch6,1, + software: 7.0.2 - **Start Date:** 2020-10-24 23:56:38 +02:00 + - **End Date:** 2020-10-24 23:56:46 +02:00 - **Creation Date:** + 2020-10-25 00:04:01 +02:00 - **Unit:** Count - **Value:** 13 + Would you like further details or analysis on these records? + """ + } +]) + +# eval_results = evaluate( +# experiment_name="AgentToolSelectionExperiment", +# dataset=dataset, +# task=evaluation_task, +# scoring_metrics=metrics, +# task_threads=1, +# ) + +second_evals = evaluate( + experiment_name="JudgeOutputExperiment", + dataset=judge_dataset, + task=evaluation_task, + scoring_metrics=[Hallucination(), LevenshteinRatio(), AnswerRelevance(require_context=False)], + task_threads=1, +) diff --git a/tests/tools_test.py b/tests/tools_test.py new file mode 100644 index 0000000..0da425f --- /dev/null +++ b/tests/tools_test.py @@ -0,0 +1,22 @@ +import asyncio + +from fastmcp import FastMCP + +from app.mcp.v1.mcp import mcp_router + + +app = FastMCP() + +@app.tool +def add_nr(a: int, b: int) -> int: + return a + b + +async def get_mcp_tools(mcp: FastMCP): + return await mcp.get_tools() + +ad = {"b": 6, "a": 5, "6": 7} +ab = {"6": 7, "a": 5, "b": 6} + +print(asyncio.run(get_mcp_tools(mcp_router))) + +print(ad == ab) \ No newline at end of file From 6500bd53b24a6131ae2e416b13aea4c3c0254288 Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 26 Sep 2025 14:03:52 +0200 Subject: [PATCH 38/77] opik costam --- .github/workflows/tests.yml | 14 +- pyproject.toml | 1 + uv.lock | 309 ++++++++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 027862a..44ea5d8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,7 @@ on: [push] env: DUCKDB_FILENAME: tests/parquet_example OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} jobs: test: @@ -41,4 +42,15 @@ jobs: run: uv run fastmcp run -t http app/main.py & - name: Run inspector run: npx @modelcontextprotocol/inspector --cli \ http://127.0.0.1:8000/mcp --method tools/list - + opik: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Run opik experiments + run: uv run tests/opik/tool_calls.py \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d8fda3d..478d0db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dev = [ "fastapi>=0.116.2", "nest-asyncio>=1.6.0", "openai-agents>=0.3.2", + "opik>=1.8.56", "pydantic-ai>=1.0.10", "pytest>=8.4.1", "pytest-asyncio>=1.0.0", diff --git a/uv.lock b/uv.lock index 8ee27c9..52773cf 100644 --- a/uv.lock +++ b/uv.lock @@ -141,6 +141,7 @@ dev = [ { name = "fastapi" }, { name = "nest-asyncio" }, { name = "openai-agents" }, + { name = "opik" }, { name = "pydantic-ai" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -175,6 +176,7 @@ dev = [ { name = "fastapi", specifier = ">=0.116.2" }, { name = "nest-asyncio", specifier = ">=1.6.0" }, { name = "openai-agents", specifier = ">=0.3.2" }, + { name = "opik", specifier = ">=1.8.56" }, { name = "pydantic-ai", specifier = ">=1.0.10" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, @@ -225,6 +227,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/a9/e7e5fe3fec60fb87bc9f8b3874c4c606e290a64b2ae8c157e08c3e69d755/boto3-1.40.38-py3-none-any.whl", hash = "sha256:fac337b4f0615e4d6ceee44686e662f51d8e57916ed2bc763468e3e8c611a658", size = 139345, upload-time = "2025-09-24T19:23:23.756Z" }, ] +[[package]] +name = "boto3-stubs" +version = "1.40.39" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore-stubs" }, + { name = "types-s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/17/ec5698d04ea4d4a285d581c0d9b0152c6f1cd89e335b211a372bef659b0e/boto3_stubs-1.40.39.tar.gz", hash = "sha256:320315400d9a1e717014b30362b8453f0957a09dc331c5aec6128c32313ca8e2", size = 100837, upload-time = "2025-09-25T19:26:03.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/36/620718640c7e28645da3059348800e1185485597661d10095f6624dad836/boto3_stubs-1.40.39-py3-none-any.whl", hash = "sha256:1f5319c486f792ff869efb255cd43b61c072c7d478e9025e08b8f98e6e16c1e4", size = 69687, upload-time = "2025-09-25T19:25:53.38Z" }, +] + +[package.optional-dependencies] +bedrock-runtime = [ + { name = "mypy-boto3-bedrock-runtime" }, +] + [[package]] name = "botocore" version = "1.40.38" @@ -239,6 +259,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/f0/ca5a00dd8fe3768ecff54756457dd0c69ed8e1cd09d0f7c21599477b5d5b/botocore-1.40.38-py3-none-any.whl", hash = "sha256:7d60a7557db3a58f9394e7ecec1f6b87495ce947eb713f29d53aee83a6e9dc71", size = 14025193, upload-time = "2025-09-24T19:23:11.093Z" }, ] +[[package]] +name = "botocore-stubs" +version = "1.40.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-awscrt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/94/16f8e1f41feaa38f1350aa5a4c60c5724b6c8524ca0e6c28523bf5070e74/botocore_stubs-1.40.33.tar.gz", hash = "sha256:89c51ae0b28d9d79fde8c497cf908ddf872ce027d2737d4d4ba473fde9cdaa82", size = 42742, upload-time = "2025-09-17T20:25:56.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/7b/6d8fe12a955b16094460e89ea7c4e063f131f4b3bd461b96bcd625d0c79e/botocore_stubs-1.40.33-py3-none-any.whl", hash = "sha256:ad21fee32cbdc7ad4730f29baf88424c7086bf88a745f8e43660ca3e9a7e5f89", size = 66843, upload-time = "2025-09-17T20:25:54.052Z" }, +] + [[package]] name = "cachetools" version = "5.5.2" @@ -697,6 +729,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0a/7a8d564b1b9909dbfc36eb93d76410a4acfada6b1e13ee451a753bb6dbc2/fastmcp-2.12.2-py3-none-any.whl", hash = "sha256:0b58d68e819c82078d1fd51989d3d81f2be7382d527308b06df55f4d0a4ec94f", size = 312029, upload-time = "2025-09-03T21:28:08.62Z" }, ] +[[package]] +name = "fastuuid" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/41/9482a375d3af33e2cdb99d3fa1bbbdb95f2e698ceb29e38880f81d919ebe/fastuuid-0.13.3-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e5be92899120006ed44b263c02588d38632b49aa1fb2a8fcd18bb5b93a1fa7f2", size = 494635, upload-time = "2025-09-25T17:18:44.961Z" }, + { url = "https://files.pythonhosted.org/packages/46/bf/50530bb9bcc505ea74c06ac376af44c2b4e085a897b2d4fb168729267418/fastuuid-0.13.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:217c8438e4b2d727c810ff4c49123e45f2109925b04463745e382c7d808472fa", size = 253079, upload-time = "2025-09-25T17:18:19.086Z" }, + { url = "https://files.pythonhosted.org/packages/0c/11/cb674d840ab86f3486cca060b93401b51a7b45eca81c269288d2efb98928/fastuuid-0.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1bffec9780ecab477f6d003dfae917720bb1485f44d15a469974e78be67d13d3", size = 244547, upload-time = "2025-09-25T17:15:05.277Z" }, + { url = "https://files.pythonhosted.org/packages/21/fd/9302cde221ec2e33f2284263e3f75758334f5cf6c93be2dcaee8ca5d717e/fastuuid-0.13.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa4744abf7203567ccc8e79c5c8585922c79a1b7d5a3bbbd7f3cb688e87192c9", size = 271469, upload-time = "2025-09-25T17:18:10.156Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f8/de9a6bc73fbd43cdc7419e1b791f2c6a1300d3638db07ba2380bf6addaf4/fastuuid-0.13.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd73098a9057f109f59cf28752b18519dd2a8c708741e26154d4c208cef9ed01", size = 272278, upload-time = "2025-09-25T17:12:26.19Z" }, + { url = "https://files.pythonhosted.org/packages/05/99/93af7a69451918ecb434a90e8e373dccc1d6bdae030bd67c24bde87df463/fastuuid-0.13.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9927b96ff5982ef18a4dccbcd1fee677faaa486b86a9f48c235f2e969b0b3956", size = 290406, upload-time = "2025-09-25T17:18:12.611Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4e/39b459085cd90cbb3c2a483fdbdeed0e4edd67c1ff036705a8486abfd29e/fastuuid-0.13.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a64486c5e52f132fb225c248c4c9bd174946514a38ab1b293595ee9bd16bb6d", size = 452823, upload-time = "2025-09-25T17:10:37.088Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b6/289aefee8cbfc8f0afaf2e9462be703a062f2686901cf25cc65ad71eba3b/fastuuid-0.13.3-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f4e9c4552e3ffaec33f8af09067dcafd435b1188a68d86cc26aef50a4b200de6", size = 468092, upload-time = "2025-09-25T17:19:11.065Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/5994c4d3a298b2116417f49abb1893950600087dbbc3839016347679c9ba/fastuuid-0.13.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:4817671973467d8c32bdc1d9dae9ad2727664ac1363dbe94e42e0a890fbc521d", size = 444968, upload-time = "2025-09-25T17:19:24.896Z" }, + { url = "https://files.pythonhosted.org/packages/89/82/31cefe573bc19f2d1b07b86af9b9db8d0a74487685dfbaf16edf4ec91055/fastuuid-0.13.3-cp313-cp313-win32.whl", hash = "sha256:ad55c13069711c3fb30c7080d117516ee7a419f64f073212c2e9a9536051b743", size = 145463, upload-time = "2025-09-25T17:17:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/f9/49/3d41776751d207526c8917d8e6ec35290f78be396f8c05102c32c0d63558/fastuuid-0.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:ffe6a759e15e0968d2022d6a07957bc1f9830127b2d1bef2ee56d4e21c5a90b6", size = 150918, upload-time = "2025-09-25T17:15:56.942Z" }, +] + [[package]] name = "filelock" version = "3.19.1" @@ -1011,6 +1061,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jiter" version = "0.11.0" @@ -1124,6 +1186,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, ] +[[package]] +name = "litellm" +version = "1.77.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pondpond" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b7/0d3c6dbcff3064238d123f90ae96764a85352f3f5caab6695a55007fd019/litellm-1.77.4.tar.gz", hash = "sha256:ce652e10ecf5b36767bfdf58e53b2802e22c3de383b03554e6ee1a4a66fa743d", size = 10330773, upload-time = "2025-09-24T17:52:44.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/32/90f8587818d146d604ed6eec95f96378363fda06b14817399cc68853383e/litellm-1.77.4-py3-none-any.whl", hash = "sha256:66c2bb776f1e19ceddfa977a2bbf7f05e6f26c4b1fec8b2093bd171d842701b8", size = 9138493, upload-time = "2025-09-24T17:52:40.764Z" }, +] + [[package]] name = "logfire" version = "4.10.0" @@ -1156,6 +1242,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/e8/4355d4909eb1f07bba1ecf7a9b99be8bbc356db828e60b750e41dbb49dab/logfire_api-4.10.0-py3-none-any.whl", hash = "sha256:20819b2f3b43a53b66a500725553bdd52ed8c74f2147aa128c5ba5aa58668059", size = 92694, upload-time = "2025-09-24T17:57:15.686Z" }, ] +[[package]] +name = "madoka" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/eb/95288b1c4aa541eb296a6271e3f8c7ece03b78923ac47dbe95d2287d9f5e/madoka-0.7.1.tar.gz", hash = "sha256:e258baa84fc0a3764365993b8bf5e1b065383a6ca8c9f862fb3e3e709843fae7", size = 81413, upload-time = "2019-02-10T18:38:01.382Z" } + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1299,6 +1391,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, ] +[[package]] +name = "mypy-boto3-bedrock-runtime" +version = "1.40.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/ff/074a1e1425d04e7294c962803655e85e20e158734534ce8d302efaa8230a/mypy_boto3_bedrock_runtime-1.40.21.tar.gz", hash = "sha256:fa9401e86d42484a53803b1dba0782d023ab35c817256e707fbe4fff88aeb881", size = 28326, upload-time = "2025-08-29T19:25:09.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/02/9d3b881bee5552600c6f456e446069d5beffd2b7862b99e1e945d60d6a9b/mypy_boto3_bedrock_runtime-1.40.21-py3-none-any.whl", hash = "sha256:4c9ea181ef00cb3d15f9b051a50e3b78272122d24cd24ac34938efe6ddfecc62", size = 34149, upload-time = "2025-08-29T19:25:03.941Z" }, +] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -1601,6 +1702,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652, upload-time = "2025-09-11T11:42:09.682Z" }, ] +[[package]] +name = "opik" +version = "1.8.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3-stubs", extra = ["bedrock-runtime"] }, + { name = "click" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "litellm" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pytest" }, + { name = "rapidfuzz" }, + { name = "rich" }, + { name = "sentry-sdk" }, + { name = "tenacity" }, + { name = "tqdm" }, + { name = "uuid6" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/11/ae34a43b8fa8bd4f39674f49e7346f509dcc77f8f5487344298017265c7e/opik-1.8.56.tar.gz", hash = "sha256:b67f74041810c7e281503ec1f0c2496e48aed014a8b8e924b5d8e74bb61b8018", size = 387852, upload-time = "2025-09-26T07:27:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/a1/8e4efe28be12503f8792a9d9d20a4a17e8a00bc93026a7c41c1ac40c28de/opik-1.8.56-py3-none-any.whl", hash = "sha256:02e34df82fb76d0314c3a00a8ce2d57ee6676a450eb10edda7a50381c5fb8045", size = 721203, upload-time = "2025-09-26T07:27:20.215Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1696,6 +1823,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/4e/a4300d52dd81b58130ccadf3873f11b3c6de54836ad4a8f32bac2bd2ba17/polars-1.33.1-cp39-abi3-win_arm64.whl", hash = "sha256:c3cfddb3b78eae01a218222bdba8048529fef7e14889a71e33a5198644427642", size = 35445171, upload-time = "2025-09-09T08:36:58.043Z" }, ] +[[package]] +name = "pondpond" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "madoka" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/9b/8411458ca8ce8b5b9b135e4a19823f1caf958ca9985883db104323492982/pondpond-1.4.1.tar.gz", hash = "sha256:8afa34b869d1434d21dd2ec12644abc3b1733fcda8fcf355300338a13a79bb7b", size = 15237, upload-time = "2024-03-01T07:08:06.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/d4/f18d6985157cc68f76469480182cbee2a03a45858456955acf57f9dcbb4c/pondpond-1.4.1-py3-none-any.whl", hash = "sha256:641028ead4e8018ca6de1220c660ddd6d6fbf62a60e72f410655dd0451d82880", size = 14498, upload-time = "2024-03-01T07:08:04.63Z" }, +] + [[package]] name = "pre-commit" version = "4.3.0" @@ -2155,6 +2294,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] +[[package]] +name = "rapidfuzz" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/fc/a98b616db9a42dcdda7c78c76bdfdf6fe290ac4c5ffbb186f73ec981ad5b/rapidfuzz-3.14.1.tar.gz", hash = "sha256:b02850e7f7152bd1edff27e9d584505b84968cacedee7a734ec4050c655a803c", size = 57869570, upload-time = "2025-09-08T21:08:15.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/f2/0024cc8eead108c4c29337abe133d72ddf3406ce9bbfbcfc110414a7ea07/rapidfuzz-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d69f470d63ee824132ecd80b1974e1d15dd9df5193916901d7860cef081a260", size = 1926515, upload-time = "2025-09-08T21:06:39.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/ae/6cb211f8930bea20fa989b23f31ee7f92940caaf24e3e510d242a1b28de4/rapidfuzz-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6f571d20152fc4833b7b5e781b36d5e4f31f3b5a596a3d53cf66a1bd4436b4f4", size = 1388431, upload-time = "2025-09-08T21:06:41.73Z" }, + { url = "https://files.pythonhosted.org/packages/39/88/bfec24da0607c39e5841ced5594ea1b907d20f83adf0e3ee87fa454a425b/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61d77e09b2b6bc38228f53b9ea7972a00722a14a6048be9a3672fb5cb08bad3a", size = 1375664, upload-time = "2025-09-08T21:06:43.737Z" }, + { url = "https://files.pythonhosted.org/packages/f4/43/9f282ba539e404bdd7052c7371d3aaaa1a9417979d2a1d8332670c7f385a/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b41d95ef86a6295d353dc3bb6c80550665ba2c3bef3a9feab46074d12a9af8f", size = 1668113, upload-time = "2025-09-08T21:06:45.758Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2f/0b3153053b1acca90969eb0867922ac8515b1a8a48706a3215c2db60e87c/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0591df2e856ad583644b40a2b99fb522f93543c65e64b771241dda6d1cfdc96b", size = 2212875, upload-time = "2025-09-08T21:06:47.447Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/623001dddc518afaa08ed1fbbfc4005c8692b7a32b0f08b20c506f17a770/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f277801f55b2f3923ef2de51ab94689a0671a4524bf7b611de979f308a54cd6f", size = 3161181, upload-time = "2025-09-08T21:06:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b7/d8404ed5ad56eb74463e5ebf0a14f0019d7eb0e65e0323f709fe72e0884c/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:893fdfd4f66ebb67f33da89eb1bd1674b7b30442fdee84db87f6cb9074bf0ce9", size = 1225495, upload-time = "2025-09-08T21:06:51.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6c/b96af62bc7615d821e3f6b47563c265fd7379d7236dfbc1cbbcce8beb1d2/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fe2651258c1f1afa9b66f44bf82f639d5f83034f9804877a1bbbae2120539ad1", size = 2396294, upload-time = "2025-09-08T21:06:53.063Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b7/c60c9d22a7debed8b8b751f506a4cece5c22c0b05e47a819d6b47bc8c14e/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ace21f7a78519d8e889b1240489cd021c5355c496cb151b479b741a4c27f0a25", size = 2529629, upload-time = "2025-09-08T21:06:55.188Z" }, + { url = "https://files.pythonhosted.org/packages/25/94/a9ec7ccb28381f14de696ffd51c321974762f137679df986f5375d35264f/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cb5acf24590bc5e57027283b015950d713f9e4d155fda5cfa71adef3b3a84502", size = 2782960, upload-time = "2025-09-08T21:06:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/04e5276d223060eca45250dbf79ea39940c0be8b3083661d58d57572c2c5/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:67ea46fa8cc78174bad09d66b9a4b98d3068e85de677e3c71ed931a1de28171f", size = 3298427, upload-time = "2025-09-08T21:06:59.319Z" }, + { url = "https://files.pythonhosted.org/packages/4a/63/24759b2a751562630b244e68ccaaf7a7525c720588fcc77c964146355aee/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:44e741d785de57d1a7bae03599c1cbc7335d0b060a35e60c44c382566e22782e", size = 4267736, upload-time = "2025-09-08T21:07:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/18/a4/73f1b1f7f44d55f40ffbffe85e529eb9d7e7f7b2ffc0931760eadd163995/rapidfuzz-3.14.1-cp313-cp313-win32.whl", hash = "sha256:b1fe6001baa9fa36bcb565e24e88830718f6c90896b91ceffcb48881e3adddbc", size = 1710515, upload-time = "2025-09-08T21:07:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8b/a8fe5a6ee4d06fd413aaa9a7e0a23a8630c4b18501509d053646d18c2aa7/rapidfuzz-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:83b8cc6336709fa5db0579189bfd125df280a554af544b2dc1c7da9cdad7e44d", size = 1540081, upload-time = "2025-09-08T21:07:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/4b0ac16c118a2367d85450b45251ee5362661e9118a1cef88aae1765ffff/rapidfuzz-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:cf75769662eadf5f9bd24e865c19e5ca7718e879273dce4e7b3b5824c4da0eb4", size = 812725, upload-time = "2025-09-08T21:07:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cb/1ad9a76d974d153783f8e0be8dbe60ec46488fac6e519db804e299e0da06/rapidfuzz-3.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d937dbeda71c921ef6537c6d41a84f1b8112f107589c9977059de57a1d726dd6", size = 1945173, upload-time = "2025-09-08T21:07:08.893Z" }, + { url = "https://files.pythonhosted.org/packages/d9/61/959ed7460941d8a81cbf6552b9c45564778a36cf5e5aa872558b30fc02b2/rapidfuzz-3.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a2d80cc1a4fcc7e259ed4f505e70b36433a63fa251f1bb69ff279fe376c5efd", size = 1413949, upload-time = "2025-09-08T21:07:11.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/f46fca44457ca1f25f23cc1f06867454fc3c3be118cd10b552b0ab3e58a2/rapidfuzz-3.14.1-cp313-cp313t-win32.whl", hash = "sha256:40875e0c06f1a388f1cab3885744f847b557e0b1642dfc31ff02039f9f0823ef", size = 1760666, upload-time = "2025-09-08T21:07:12.884Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d0/7a5d9c04446f8b66882b0fae45b36a838cf4d31439b5d1ab48a9d17c8e57/rapidfuzz-3.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:876dc0c15552f3d704d7fb8d61bdffc872ff63bedf683568d6faad32e51bbce8", size = 1579760, upload-time = "2025-09-08T21:07:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/4e/aa/2c03ae112320d0746f2c869cae68c413f3fe3b6403358556f2b747559723/rapidfuzz-3.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:61458e83b0b3e2abc3391d0953c47d6325e506ba44d6a25c869c4401b3bc222c", size = 832088, upload-time = "2025-09-08T21:07:17.03Z" }, + { url = "https://files.pythonhosted.org/packages/d6/36/53debca45fbe693bd6181fb05b6a2fd561c87669edb82ec0d7c1961a43f0/rapidfuzz-3.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e84d9a844dc2e4d5c4cabd14c096374ead006583304333c14a6fbde51f612a44", size = 1926336, upload-time = "2025-09-08T21:07:18.809Z" }, + { url = "https://files.pythonhosted.org/packages/ae/32/b874f48609665fcfeaf16cbaeb2bbc210deef2b88e996c51cfc36c3eb7c3/rapidfuzz-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:40301b93b99350edcd02dbb22e37ca5f2a75d0db822e9b3c522da451a93d6f27", size = 1389653, upload-time = "2025-09-08T21:07:20.667Z" }, + { url = "https://files.pythonhosted.org/packages/97/25/f6c5a1ff4ec11edadacb270e70b8415f51fa2f0d5730c2c552b81651fbe3/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fedd5097a44808dddf341466866e5c57a18a19a336565b4ff50aa8f09eb528f6", size = 1380911, upload-time = "2025-09-08T21:07:22.584Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/d322202ef8fab463759b51ebfaa33228100510c82e6153bd7a922e150270/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e3e61c9e80d8c26709d8aa5c51fdd25139c81a4ab463895f8a567f8347b0548", size = 1673515, upload-time = "2025-09-08T21:07:24.417Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b9/6b2a97f4c6be96cac3749f32301b8cdf751ce5617b1c8934c96586a0662b/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da011a373722fac6e64687297a1d17dc8461b82cb12c437845d5a5b161bc24b9", size = 2219394, upload-time = "2025-09-08T21:07:26.402Z" }, + { url = "https://files.pythonhosted.org/packages/11/bf/afb76adffe4406e6250f14ce48e60a7eb05d4624945bd3c044cfda575fbc/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5967d571243cfb9ad3710e6e628ab68c421a237b76e24a67ac22ee0ff12784d6", size = 3163582, upload-time = "2025-09-08T21:07:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/e6405227560f61e956cb4c5de653b0f874751c5ada658d3532d6c1df328e/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:474f416cbb9099676de54aa41944c154ba8d25033ee460f87bb23e54af6d01c9", size = 1221116, upload-time = "2025-09-08T21:07:30.8Z" }, + { url = "https://files.pythonhosted.org/packages/55/e6/5b757e2e18de384b11d1daf59608453f0baf5d5d8d1c43e1a964af4dc19a/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae2d57464b59297f727c4e201ea99ec7b13935f1f056c753e8103da3f2fc2404", size = 2402670, upload-time = "2025-09-08T21:07:32.702Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/d753a415fe54531aa882e288db5ed77daaa72e05c1a39e1cbac00d23024f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:57047493a1f62f11354c7143c380b02f1b355c52733e6b03adb1cb0fe8fb8816", size = 2521659, upload-time = "2025-09-08T21:07:35.218Z" }, + { url = "https://files.pythonhosted.org/packages/cd/28/d4e7fe1515430db98f42deb794c7586a026d302fe70f0216b638d89cf10f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4acc20776f225ee37d69517a237c090b9fa7e0836a0b8bc58868e9168ba6ef6f", size = 2788552, upload-time = "2025-09-08T21:07:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/eab05473af7a2cafb4f3994bc6bf408126b8eec99a569aac6254ac757db4/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4373f914ff524ee0146919dea96a40a8200ab157e5a15e777a74a769f73d8a4a", size = 3306261, upload-time = "2025-09-08T21:07:39.624Z" }, + { url = "https://files.pythonhosted.org/packages/d1/31/2feb8dfcfcff6508230cd2ccfdde7a8bf988c6fda142fe9ce5d3eb15704d/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:37017b84953927807847016620d61251fe236bd4bcb25e27b6133d955bb9cafb", size = 4269522, upload-time = "2025-09-08T21:07:41.663Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/250538d73c8fbab60597c3d131a11ef2a634d38b44296ca11922794491ac/rapidfuzz-3.14.1-cp314-cp314-win32.whl", hash = "sha256:c8d1dd1146539e093b84d0805e8951475644af794ace81d957ca612e3eb31598", size = 1745018, upload-time = "2025-09-08T21:07:44.313Z" }, + { url = "https://files.pythonhosted.org/packages/c5/15/d50839d20ad0743aded25b08a98ffb872f4bfda4e310bac6c111fcf6ea1f/rapidfuzz-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:f51c7571295ea97387bac4f048d73cecce51222be78ed808263b45c79c40a440", size = 1587666, upload-time = "2025-09-08T21:07:46.917Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ff/d73fec989213fb6f0b6f15ee4bbdf2d88b0686197951a06b036111cd1c7d/rapidfuzz-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:01eab10ec90912d7d28b3f08f6c91adbaf93458a53f849ff70776ecd70dd7a7a", size = 835780, upload-time = "2025-09-08T21:07:49.256Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e7/f0a242687143cebd33a1fb165226b73bd9496d47c5acfad93de820a18fa8/rapidfuzz-3.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:60879fcae2f7618403c4c746a9a3eec89327d73148fb6e89a933b78442ff0669", size = 1945182, upload-time = "2025-09-08T21:07:51.84Z" }, + { url = "https://files.pythonhosted.org/packages/96/29/ca8a3f8525e3d0e7ab49cb927b5fb4a54855f794c9ecd0a0b60a6c96a05f/rapidfuzz-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f94d61e44db3fc95a74006a394257af90fa6e826c900a501d749979ff495d702", size = 1413946, upload-time = "2025-09-08T21:07:53.702Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ef/6fd10aa028db19c05b4ac7fe77f5613e4719377f630c709d89d7a538eea2/rapidfuzz-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:93b6294a3ffab32a9b5f9b5ca048fa0474998e7e8bb0f2d2b5e819c64cb71ec7", size = 1795851, upload-time = "2025-09-08T21:07:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/e4/30/acd29ebd906a50f9e0f27d5f82a48cf5e8854637b21489bd81a2459985cf/rapidfuzz-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6cb56b695421538fdbe2c0c85888b991d833b8637d2f2b41faa79cea7234c000", size = 1626748, upload-time = "2025-09-08T21:07:58.166Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f4/dfc7b8c46b1044a47f7ca55deceb5965985cff3193906cb32913121e6652/rapidfuzz-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7cd312c380d3ce9d35c3ec9726b75eee9da50e8a38e89e229a03db2262d3d96b", size = 853771, upload-time = "2025-09-08T21:08:00.816Z" }, +] + [[package]] name = "referencing" version = "0.36.2" @@ -2168,6 +2355,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] +[[package]] +name = "regex" +version = "2025.9.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/d3/eaa0d28aba6ad1827ad1e716d9a93e1ba963ada61887498297d3da715133/regex-2025.9.18.tar.gz", hash = "sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4", size = 400917, upload-time = "2025-09-19T00:38:35.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/c7/5c48206a60ce33711cf7dcaeaed10dd737733a3569dc7e1dce324dd48f30/regex-2025.9.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2", size = 485955, upload-time = "2025-09-19T00:36:26.822Z" }, + { url = "https://files.pythonhosted.org/packages/e9/be/74fc6bb19a3c491ec1ace943e622b5a8539068771e8705e469b2da2306a7/regex-2025.9.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb", size = 289583, upload-time = "2025-09-19T00:36:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/25/c4/9ceaa433cb5dc515765560f22a19578b95b92ff12526e5a259321c4fc1a0/regex-2025.9.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af", size = 287000, upload-time = "2025-09-19T00:36:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e6/68bc9393cb4dc68018456568c048ac035854b042bc7c33cb9b99b0680afa/regex-2025.9.18-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29", size = 797535, upload-time = "2025-09-19T00:36:31.876Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/ebae9032d34b78ecfe9bd4b5e6575b55351dc8513485bb92326613732b8c/regex-2025.9.18-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f", size = 862603, upload-time = "2025-09-19T00:36:33.344Z" }, + { url = "https://files.pythonhosted.org/packages/3b/74/12332c54b3882557a4bcd2b99f8be581f5c6a43cf1660a85b460dd8ff468/regex-2025.9.18-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68", size = 910829, upload-time = "2025-09-19T00:36:34.826Z" }, + { url = "https://files.pythonhosted.org/packages/86/70/ba42d5ed606ee275f2465bfc0e2208755b06cdabd0f4c7c4b614d51b57ab/regex-2025.9.18-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783", size = 802059, upload-time = "2025-09-19T00:36:36.664Z" }, + { url = "https://files.pythonhosted.org/packages/da/c5/fcb017e56396a7f2f8357412638d7e2963440b131a3ca549be25774b3641/regex-2025.9.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac", size = 786781, upload-time = "2025-09-19T00:36:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ee/21c4278b973f630adfb3bcb23d09d83625f3ab1ca6e40ebdffe69901c7a1/regex-2025.9.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e", size = 856578, upload-time = "2025-09-19T00:36:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/de51550dc7274324435c8f1539373ac63019b0525ad720132866fff4a16a/regex-2025.9.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23", size = 849119, upload-time = "2025-09-19T00:36:41.651Z" }, + { url = "https://files.pythonhosted.org/packages/60/52/383d3044fc5154d9ffe4321696ee5b2ee4833a28c29b137c22c33f41885b/regex-2025.9.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f", size = 788219, upload-time = "2025-09-19T00:36:43.575Z" }, + { url = "https://files.pythonhosted.org/packages/20/bd/2614fc302671b7359972ea212f0e3a92df4414aaeacab054a8ce80a86073/regex-2025.9.18-cp313-cp313-win32.whl", hash = "sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d", size = 264517, upload-time = "2025-09-19T00:36:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/ab5c1581e6563a7bffdc1974fb2d25f05689b88e2d416525271f232b1946/regex-2025.9.18-cp313-cp313-win_amd64.whl", hash = "sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d", size = 275481, upload-time = "2025-09-19T00:36:46.965Z" }, + { url = "https://files.pythonhosted.org/packages/49/22/ee47672bc7958f8c5667a587c2600a4fba8b6bab6e86bd6d3e2b5f7cac42/regex-2025.9.18-cp313-cp313-win_arm64.whl", hash = "sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb", size = 268598, upload-time = "2025-09-19T00:36:48.314Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/6887e16a187c6226cb85d8301e47d3b73ecc4505a3a13d8da2096b44fd76/regex-2025.9.18-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2", size = 489765, upload-time = "2025-09-19T00:36:49.996Z" }, + { url = "https://files.pythonhosted.org/packages/51/c5/e2f7325301ea2916ff301c8d963ba66b1b2c1b06694191df80a9c4fea5d0/regex-2025.9.18-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3", size = 291228, upload-time = "2025-09-19T00:36:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/7d229d2bc6961289e864a3a3cfebf7d0d250e2e65323a8952cbb7e22d824/regex-2025.9.18-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12", size = 289270, upload-time = "2025-09-19T00:36:53.118Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/b4f06868ee2958ff6430df89857fbf3d43014bbf35538b6ec96c2704e15d/regex-2025.9.18-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0", size = 806326, upload-time = "2025-09-19T00:36:54.631Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e4/bca99034a8f1b9b62ccf337402a8e5b959dd5ba0e5e5b2ead70273df3277/regex-2025.9.18-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6", size = 871556, upload-time = "2025-09-19T00:36:56.208Z" }, + { url = "https://files.pythonhosted.org/packages/6d/df/e06ffaf078a162f6dd6b101a5ea9b44696dca860a48136b3ae4a9caf25e2/regex-2025.9.18-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef", size = 913817, upload-time = "2025-09-19T00:36:57.807Z" }, + { url = "https://files.pythonhosted.org/packages/9e/05/25b05480b63292fd8e84800b1648e160ca778127b8d2367a0a258fa2e225/regex-2025.9.18-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a", size = 811055, upload-time = "2025-09-19T00:36:59.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/97/7bc7574655eb651ba3a916ed4b1be6798ae97af30104f655d8efd0cab24b/regex-2025.9.18-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d", size = 794534, upload-time = "2025-09-19T00:37:01.405Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c2/d5da49166a52dda879855ecdba0117f073583db2b39bb47ce9a3378a8e9e/regex-2025.9.18-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368", size = 866684, upload-time = "2025-09-19T00:37:03.441Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2d/0a5c4e6ec417de56b89ff4418ecc72f7e3feca806824c75ad0bbdae0516b/regex-2025.9.18-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90", size = 853282, upload-time = "2025-09-19T00:37:04.985Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/d656af63e31a86572ec829665d6fa06eae7e144771e0330650a8bb865635/regex-2025.9.18-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7", size = 797830, upload-time = "2025-09-19T00:37:06.697Z" }, + { url = "https://files.pythonhosted.org/packages/db/ce/06edc89df8f7b83ffd321b6071be4c54dc7332c0f77860edc40ce57d757b/regex-2025.9.18-cp313-cp313t-win32.whl", hash = "sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e", size = 267281, upload-time = "2025-09-19T00:37:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/83/9a/2b5d9c8b307a451fd17068719d971d3634ca29864b89ed5c18e499446d4a/regex-2025.9.18-cp313-cp313t-win_amd64.whl", hash = "sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730", size = 278724, upload-time = "2025-09-19T00:37:10.023Z" }, + { url = "https://files.pythonhosted.org/packages/3d/70/177d31e8089a278a764f8ec9a3faac8d14a312d622a47385d4b43905806f/regex-2025.9.18-cp313-cp313t-win_arm64.whl", hash = "sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a", size = 269771, upload-time = "2025-09-19T00:37:13.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/b7/3b4663aa3b4af16819f2ab6a78c4111c7e9b066725d8107753c2257448a5/regex-2025.9.18-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129", size = 486130, upload-time = "2025-09-19T00:37:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/4533f5d7ac9c6a02a4725fe8883de2aebc713e67e842c04cf02626afb747/regex-2025.9.18-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea", size = 289539, upload-time = "2025-09-19T00:37:16.356Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8d/5ab6797c2750985f79e9995fad3254caa4520846580f266ae3b56d1cae58/regex-2025.9.18-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1", size = 287233, upload-time = "2025-09-19T00:37:18.025Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/95afcb02ba8d3a64e6ffeb801718ce73471ad6440c55d993f65a4a5e7a92/regex-2025.9.18-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47", size = 797876, upload-time = "2025-09-19T00:37:19.609Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fb/720b1f49cec1f3b5a9fea5b34cd22b88b5ebccc8c1b5de9cc6f65eed165a/regex-2025.9.18-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379", size = 863385, upload-time = "2025-09-19T00:37:21.65Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ca/e0d07ecf701e1616f015a720dc13b84c582024cbfbb3fc5394ae204adbd7/regex-2025.9.18-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203", size = 910220, upload-time = "2025-09-19T00:37:23.723Z" }, + { url = "https://files.pythonhosted.org/packages/b6/45/bba86413b910b708eca705a5af62163d5d396d5f647ed9485580c7025209/regex-2025.9.18-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164", size = 801827, upload-time = "2025-09-19T00:37:25.684Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/740fbd9fcac31a1305a8eed30b44bf0f7f1e042342be0a4722c0365ecfca/regex-2025.9.18-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb", size = 786843, upload-time = "2025-09-19T00:37:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/80/a7/0579e8560682645906da640c9055506465d809cb0f5415d9976f417209a6/regex-2025.9.18-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743", size = 857430, upload-time = "2025-09-19T00:37:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9b/4dc96b6c17b38900cc9fee254fc9271d0dde044e82c78c0811b58754fde5/regex-2025.9.18-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282", size = 848612, upload-time = "2025-09-19T00:37:31.42Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6a/6f659f99bebb1775e5ac81a3fb837b85897c1a4ef5acffd0ff8ffe7e67fb/regex-2025.9.18-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773", size = 787967, upload-time = "2025-09-19T00:37:34.019Z" }, + { url = "https://files.pythonhosted.org/packages/61/35/9e35665f097c07cf384a6b90a1ac11b0b1693084a0b7a675b06f760496c6/regex-2025.9.18-cp314-cp314-win32.whl", hash = "sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788", size = 269847, upload-time = "2025-09-19T00:37:35.759Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/27594dbe0f1590b82de2821ebfe9a359b44dcb9b65524876cd12fabc447b/regex-2025.9.18-cp314-cp314-win_amd64.whl", hash = "sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3", size = 278755, upload-time = "2025-09-19T00:37:37.367Z" }, + { url = "https://files.pythonhosted.org/packages/30/a3/0cd8d0d342886bd7d7f252d701b20ae1a3c72dc7f34ef4b2d17790280a09/regex-2025.9.18-cp314-cp314-win_arm64.whl", hash = "sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d", size = 271873, upload-time = "2025-09-19T00:37:39.125Z" }, + { url = "https://files.pythonhosted.org/packages/99/cb/8a1ab05ecf404e18b54348e293d9b7a60ec2bd7aa59e637020c5eea852e8/regex-2025.9.18-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306", size = 489773, upload-time = "2025-09-19T00:37:40.968Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/6543c9b7f7e734d2404fa2863d0d710c907bef99d4598760ed4563d634c3/regex-2025.9.18-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946", size = 291221, upload-time = "2025-09-19T00:37:42.901Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/e9fdee6ad6bf708d98c5d17fded423dcb0661795a49cba1b4ffb8358377a/regex-2025.9.18-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f", size = 289268, upload-time = "2025-09-19T00:37:44.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/a6/bc3e8a918abe4741dadeaeb6c508e3a4ea847ff36030d820d89858f96a6c/regex-2025.9.18-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95", size = 806659, upload-time = "2025-09-19T00:37:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/2b/71/ea62dbeb55d9e6905c7b5a49f75615ea1373afcad95830047e4e310db979/regex-2025.9.18-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b", size = 871701, upload-time = "2025-09-19T00:37:48.882Z" }, + { url = "https://files.pythonhosted.org/packages/6a/90/fbe9dedb7dad24a3a4399c0bae64bfa932ec8922a0a9acf7bc88db30b161/regex-2025.9.18-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3", size = 913742, upload-time = "2025-09-19T00:37:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/47e4a8c0e73d41eb9eb9fdeba3b1b810110a5139a2526e82fd29c2d9f867/regex-2025.9.18-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571", size = 811117, upload-time = "2025-09-19T00:37:52.686Z" }, + { url = "https://files.pythonhosted.org/packages/2a/da/435f29fddfd015111523671e36d30af3342e8136a889159b05c1d9110480/regex-2025.9.18-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad", size = 794647, upload-time = "2025-09-19T00:37:54.626Z" }, + { url = "https://files.pythonhosted.org/packages/23/66/df5e6dcca25c8bc57ce404eebc7342310a0d218db739d7882c9a2b5974a3/regex-2025.9.18-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494", size = 866747, upload-time = "2025-09-19T00:37:56.367Z" }, + { url = "https://files.pythonhosted.org/packages/82/42/94392b39b531f2e469b2daa40acf454863733b674481fda17462a5ffadac/regex-2025.9.18-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b", size = 853434, upload-time = "2025-09-19T00:37:58.39Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f8/dcc64c7f7bbe58842a8f89622b50c58c3598fbbf4aad0a488d6df2c699f1/regex-2025.9.18-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41", size = 798024, upload-time = "2025-09-19T00:38:00.397Z" }, + { url = "https://files.pythonhosted.org/packages/20/8d/edf1c5d5aa98f99a692313db813ec487732946784f8f93145e0153d910e5/regex-2025.9.18-cp314-cp314t-win32.whl", hash = "sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096", size = 273029, upload-time = "2025-09-19T00:38:02.383Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/02d4e4f88466f17b145f7ea2b2c11af3a942db6222429c2c146accf16054/regex-2025.9.18-cp314-cp314t-win_amd64.whl", hash = "sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a", size = 282680, upload-time = "2025-09-19T00:38:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a3/c64894858aaaa454caa7cc47e2f225b04d3ed08ad649eacf58d45817fad2/regex-2025.9.18-cp314-cp314t-win_arm64.whl", hash = "sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01", size = 273034, upload-time = "2025-09-19T00:38:05.807Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -2337,6 +2588,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, ] +[[package]] +name = "sentry-sdk" +version = "2.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/72/43294fa4bdd75c51610b5104a3ff834459ba653abb415150aa7826a249dd/sentry_sdk-2.39.0.tar.gz", hash = "sha256:8c185854d111f47f329ab6bc35993f28f7a6b7114db64aa426b326998cfa14e9", size = 348556, upload-time = "2025-09-25T09:15:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/44/4356cc64246ba7b2b920f7c97a85c3c52748e213e250b512ee8152eb559d/sentry_sdk-2.39.0-py2.py3-none-any.whl", hash = "sha256:ba655ca5e57b41569b18e2a5552cb3375209760a5d332cdd87c6c3f28f729602", size = 370851, upload-time = "2025-09-25T09:15:36.35Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -2469,6 +2733,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/ac/03bbe688c090b5e16b507b4e36d7c4e5d95e2a0861dd77922801088edfb1/testcontainers_postgres-0.0.1rc1-py3-none-any.whl", hash = "sha256:1bd0afcff2c236c08ffbf3e4926e713d8c58e20df82c31e62fb9cca70582fd5a", size = 2906, upload-time = "2023-01-06T16:37:45.675Z" }, ] +[[package]] +name = "tiktoken" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/86/ad0155a37c4f310935d5ac0b1ccf9bdb635dcb906e0a9a26b616dd55825a/tiktoken-0.11.0.tar.gz", hash = "sha256:3c518641aee1c52247c2b97e74d8d07d780092af79d5911a6ab5e79359d9b06a", size = 37648, upload-time = "2025-08-08T23:58:08.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/cd/a9034bcee638716d9310443818d73c6387a6a96db93cbcb0819b77f5b206/tiktoken-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a5f3f25ffb152ee7fec78e90a5e5ea5b03b4ea240beed03305615847f7a6ace2", size = 1055339, upload-time = "2025-08-08T23:57:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/f1/91/9922b345f611b4e92581f234e64e9661e1c524875c8eadd513c4b2088472/tiktoken-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dc6e9ad16a2a75b4c4be7208055a1f707c9510541d94d9cc31f7fbdc8db41d8", size = 997080, upload-time = "2025-08-08T23:57:53.442Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9d/49cd047c71336bc4b4af460ac213ec1c457da67712bde59b892e84f1859f/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a0517634d67a8a48fd4a4ad73930c3022629a85a217d256a6e9b8b47439d1e4", size = 1128501, upload-time = "2025-08-08T23:57:54.808Z" }, + { url = "https://files.pythonhosted.org/packages/52/d5/a0dcdb40dd2ea357e83cb36258967f0ae96f5dd40c722d6e382ceee6bba9/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fb4effe60574675118b73c6fbfd3b5868e5d7a1f570d6cc0d18724b09ecf318", size = 1182743, upload-time = "2025-08-08T23:57:56.307Z" }, + { url = "https://files.pythonhosted.org/packages/3b/17/a0fc51aefb66b7b5261ca1314afa83df0106b033f783f9a7bcbe8e741494/tiktoken-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94f984c9831fd32688aef4348803b0905d4ae9c432303087bae370dc1381a2b8", size = 1244057, upload-time = "2025-08-08T23:57:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/50/79/bcf350609f3a10f09fe4fc207f132085e497fdd3612f3925ab24d86a0ca0/tiktoken-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2177ffda31dec4023356a441793fed82f7af5291120751dee4d696414f54db0c", size = 883901, upload-time = "2025-08-08T23:57:59.359Z" }, +] + [[package]] name = "tokenizers" version = "0.22.1" @@ -2531,6 +2813,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/36/5a3a70c5d497d3332f9e63cabc9c6f13484783b832fecc393f4f1c0c4aa8/ty-0.0.1a20-py3-none-win_arm64.whl", hash = "sha256:d8ac1c5a14cda5fad1a8b53959d9a5d979fe16ce1cc2785ea8676fed143ac85f", size = 8269906, upload-time = "2025-09-03T12:35:45.045Z" }, ] +[[package]] +name = "types-awscrt" +version = "0.27.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/ce/5d84526a39f44c420ce61b16654193f8437d74b54f21597ea2ac65d89954/types_awscrt-0.27.6.tar.gz", hash = "sha256:9d3f1865a93b8b2c32f137514ac88cb048b5bc438739945ba19d972698995bfb", size = 16937, upload-time = "2025-08-13T01:54:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/af/e3d20e3e81d235b3964846adf46a334645a8a9b25a0d3d472743eb079552/types_awscrt-0.27.6-py3-none-any.whl", hash = "sha256:18aced46da00a57f02eb97637a32e5894dc5aa3dc6a905ba3e5ed85b9f3c526b", size = 39626, upload-time = "2025-08-13T01:54:53.454Z" }, +] + [[package]] name = "types-protobuf" version = "6.32.1.20250918" @@ -2552,6 +2843,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, ] +[[package]] +name = "types-s3transfer" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/c5/23946fac96c9dd5815ec97afd1c8ad6d22efa76c04a79a4823f2f67692a5/types_s3transfer-0.13.1.tar.gz", hash = "sha256:ce488d79fdd7d3b9d39071939121eca814ec65de3aa36bdce1f9189c0a61cc80", size = 14181, upload-time = "2025-08-31T16:57:06.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/dc/b3f9b5c93eed6ffe768f4972661250584d5e4f248b548029026964373bcd/types_s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:4ff730e464a3fd3785b5541f0f555c1bd02ad408cf82b6b7a95429f6b0d26b4a", size = 19617, upload-time = "2025-08-31T16:57:05.73Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -2591,6 +2891,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] +[[package]] +name = "uuid6" +version = "2025.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/b7/4c0f736ca824b3a25b15e8213d1bcfc15f8ac2ae48d1b445b310892dc4da/uuid6-2025.0.1.tar.gz", hash = "sha256:cd0af94fa428675a44e32c5319ec5a3485225ba2179eefcf4c3f205ae30a81bd", size = 13932, upload-time = "2025-07-04T18:30:35.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b2/93faaab7962e2aa8d6e174afb6f76be2ca0ce89fde14d3af835acebcaa59/uuid6-2025.0.1-py3-none-any.whl", hash = "sha256:80530ce4d02a93cdf82e7122ca0da3ebbbc269790ec1cb902481fa3e9cc9ff99", size = 6979, upload-time = "2025-07-04T18:30:34.001Z" }, +] + [[package]] name = "uvicorn" version = "0.35.0" From 31f94b128e64649c612f3894c32f7d3b2f78dfc5 Mon Sep 17 00:00:00 2001 From: czajkub Date: Fri, 26 Sep 2025 14:19:51 +0200 Subject: [PATCH 39/77] comemgnae --- tests/opik/tool_calls.py | 76 ++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index 6d12b74..d897180 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -95,44 +95,44 @@ def evaluation_task(dataset_item): judge_dataset = client.get_or_create_dataset(name="output_checks") -judge_dataset.insert([ - { - "input": "search for step count records between 65 and 90 steps", - "expected_output": """ - I found some step count records with values between 65 and 90 steps: - 1. **76 Steps** - **Source:** Rob’s Apple Watch - **Device:** - Apple Watch, model Watch6,1 - **Date:** October 25, 2020 - - **Start Time:** 00:05:13 - **End Time:** 00:06:14 - 2. **87 Steps** - **Source:** Rob’s Apple Watch - - **Device:** Apple Watch, model Watch6,1 - - **Date:** October 25, 2020 - **Start Time:** 00:06:14 - - **End Time:** 00:07:15 3. **66 Steps** - **Source:** - Rob’s Apple Watch - **Device:** Apple Watch, model Watch6,1 - - **Date:** October 25, 2020 - **Start Time:** 00:11:35 - - **End Time:** 00:12:37 If you need more details or additional - records, feel free to ask! - """ - }, - { - "input": "can you search for step records with exactly 13 steps", - "expected_output": """ - I found two records of step count with exactly 13 steps. - Here are the details: 1. **Record 1:** - **Source Name:** - Rob’s Apple Watch - **Source Version:** 7.0.2 - **Device:** - Apple Watch, manufacturer: Apple Inc., model: Watch, hardware: - Watch6,1, software: 7.0.2 - **Start Date:** 2020-10-24 - 23:54:51 +02:00 - **End Date:** 2020-10-24 23:55:30 +02:00 - - **Creation Date:** 2020-10-25 00:04:01 +02:00 - **Unit:** - Count - **Value:** 13 2. **Record 2:** - **Source Name:** - Rob’s Apple Watch - **Source Version:** 7.0.2 - **Device:** Apple - Watch, manufacturer: Apple Inc., model: Watch, hardware: Watch6,1, - software: 7.0.2 - **Start Date:** 2020-10-24 23:56:38 +02:00 - - **End Date:** 2020-10-24 23:56:46 +02:00 - **Creation Date:** - 2020-10-25 00:04:01 +02:00 - **Unit:** Count - **Value:** 13 - Would you like further details or analysis on these records? - """ - } -]) +# judge_dataset.insert([ +# { +# "input": "search for step count records between 65 and 90 steps", +# "expected_output": """ +# I found some step count records with values between 65 and 90 steps: +# 1. **76 Steps** - **Source:** Rob’s Apple Watch - **Device:** +# Apple Watch, model Watch6,1 - **Date:** October 25, 2020 +# - **Start Time:** 00:05:13 - **End Time:** 00:06:14 +# 2. **87 Steps** - **Source:** Rob’s Apple Watch +# - **Device:** Apple Watch, model Watch6,1 +# - **Date:** October 25, 2020 - **Start Time:** 00:06:14 +# - **End Time:** 00:07:15 3. **66 Steps** - **Source:** +# Rob’s Apple Watch - **Device:** Apple Watch, model Watch6,1 +# - **Date:** October 25, 2020 - **Start Time:** 00:11:35 - +# **End Time:** 00:12:37 If you need more details or additional +# records, feel free to ask! +# """ +# }, +# { +# "input": "can you search for step records with exactly 13 steps", +# "expected_output": """ +# I found two records of step count with exactly 13 steps. +# Here are the details: 1. **Record 1:** - **Source Name:** +# Rob’s Apple Watch - **Source Version:** 7.0.2 - **Device:** +# Apple Watch, manufacturer: Apple Inc., model: Watch, hardware: +# Watch6,1, software: 7.0.2 - **Start Date:** 2020-10-24 +# 23:54:51 +02:00 - **End Date:** 2020-10-24 23:55:30 +02:00 +# - **Creation Date:** 2020-10-25 00:04:01 +02:00 - **Unit:** +# Count - **Value:** 13 2. **Record 2:** - **Source Name:** +# Rob’s Apple Watch - **Source Version:** 7.0.2 - **Device:** Apple +# Watch, manufacturer: Apple Inc., model: Watch, hardware: Watch6,1, +# software: 7.0.2 - **Start Date:** 2020-10-24 23:56:38 +02:00 +# - **End Date:** 2020-10-24 23:56:46 +02:00 - **Creation Date:** +# 2020-10-25 00:04:01 +02:00 - **Unit:** Count - **Value:** 13 +# Would you like further details or analysis on these records? +# """ +# } +# ]) # eval_results = evaluate( # experiment_name="AgentToolSelectionExperiment", From 4a3356f404b669cdb8b54b11130f6ee38489df09 Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 09:17:07 +0200 Subject: [PATCH 40/77] curl mcp server --- .github/workflows/tests.yml | 2 + tests/opik/tool_calls.py | 85 ++++++++++++++++--------------------- 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 44ea5d8..31603b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,6 +40,8 @@ jobs: run: uv sync --group dev - name: Run mcp server run: uv run fastmcp run -t http app/main.py & + - run: sleep 5 + - run: curl http://127.0.0.1:8000/mcp - name: Run inspector run: npx @modelcontextprotocol/inspector --cli \ http://127.0.0.1:8000/mcp --method tools/list opik: diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index d897180..f96b51b 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -9,11 +9,9 @@ LevenshteinRatio, AnswerRelevance ) -import nest_asyncio from tests.agent import AgentManager -nest_asyncio.apply() agent_manager = AgentManager() asyncio.run(agent_manager.initialize()) @@ -83,13 +81,17 @@ def evaluation_task(dataset_item): dataset = client.get_dataset(name="tool_calls") dataset.insert([ - # { - # "input": "search for step count records between 65 and 90 steps", - # "tool_call": "search_health_records_duckdb" - # }, { - "input": "can you search for step records with exactly 13 steps", - "tool_call": "search_values_duckdb" + "input": "give me a summary of my health from duckdb", + "tool_call": "get_health_summary_duckdb" + }, + { + "input": "give me some statistics about my heart rate", + "tool_call": "get_statistics_by_type_duckdb" + }, + { + "input": "give me trend data for my step count in october 2024 from duckdb", + "tool_call": "get_trend_data_duckdb" } ]) @@ -97,55 +99,42 @@ def evaluation_task(dataset_item): # judge_dataset.insert([ # { -# "input": "search for step count records between 65 and 90 steps", +# "input": "give me a summary of my health from duckdb", # "expected_output": """ -# I found some step count records with values between 65 and 90 steps: -# 1. **76 Steps** - **Source:** Rob’s Apple Watch - **Device:** -# Apple Watch, model Watch6,1 - **Date:** October 25, 2020 -# - **Start Time:** 00:05:13 - **End Time:** 00:06:14 -# 2. **87 Steps** - **Source:** Rob’s Apple Watch -# - **Device:** Apple Watch, model Watch6,1 -# - **Date:** October 25, 2020 - **Start Time:** 00:06:14 -# - **End Time:** 00:07:15 3. **66 Steps** - **Source:** -# Rob’s Apple Watch - **Device:** Apple Watch, model Watch6,1 -# - **Date:** October 25, 2020 - **Start Time:** 00:11:35 - -# **End Time:** 00:12:37 If you need more details or additional -# records, feel free to ask! +# Here is a summary of your health data from DuckDB: +# - **Basal Energy Burned**: 18 records - **Heart Rate**: +# 17 records - **Step Count**: 10 records - **Body Mass Index +# (BMI)**: 8 records - **Dietary Water**: 1 record If you +# need more detailed information or specific statistics +# on any of these categories, feel free to ask! # """ # }, # { -# "input": "can you search for step records with exactly 13 steps", +# "input": "give me some statistics about my heart rate", # "expected_output": """ -# I found two records of step count with exactly 13 steps. -# Here are the details: 1. **Record 1:** - **Source Name:** -# Rob’s Apple Watch - **Source Version:** 7.0.2 - **Device:** -# Apple Watch, manufacturer: Apple Inc., model: Watch, hardware: -# Watch6,1, software: 7.0.2 - **Start Date:** 2020-10-24 -# 23:54:51 +02:00 - **End Date:** 2020-10-24 23:55:30 +02:00 -# - **Creation Date:** 2020-10-25 00:04:01 +02:00 - **Unit:** -# Count - **Value:** 13 2. **Record 2:** - **Source Name:** -# Rob’s Apple Watch - **Source Version:** 7.0.2 - **Device:** Apple -# Watch, manufacturer: Apple Inc., model: Watch, hardware: Watch6,1, -# software: 7.0.2 - **Start Date:** 2020-10-24 23:56:38 +02:00 -# - **End Date:** 2020-10-24 23:56:46 +02:00 - **Creation Date:** -# 2020-10-25 00:04:01 +02:00 - **Unit:** Count - **Value:** 13 -# Would you like further details or analysis on these records? +# idk yet +# """ +# }, +# { +# "input": "give me trend data for my step count in october 2024 from duckdb", +# "expected_output": """ +# idk yet # """ # } # ]) -# eval_results = evaluate( -# experiment_name="AgentToolSelectionExperiment", -# dataset=dataset, -# task=evaluation_task, -# scoring_metrics=metrics, -# task_threads=1, -# ) - -second_evals = evaluate( - experiment_name="JudgeOutputExperiment", - dataset=judge_dataset, +eval_results = evaluate( + experiment_name="AgentToolSelectionExperiment", + dataset=dataset, task=evaluation_task, - scoring_metrics=[Hallucination(), LevenshteinRatio(), AnswerRelevance(require_context=False)], + scoring_metrics=metrics, task_threads=1, ) + +# second_evals = evaluate( +# experiment_name="JudgeOutputExperiment", +# dataset=judge_dataset, +# task=evaluation_task, +# scoring_metrics=[Hallucination(), LevenshteinRatio(), AnswerRelevance(require_context=False)], +# task_threads=1, +# ) From c6e7374df40b76c16691a1281c72614ce786d520 Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 09:27:43 +0200 Subject: [PATCH 41/77] typo.. --- .github/workflows/tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31603b3..f03c131 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,9 +41,8 @@ jobs: - name: Run mcp server run: uv run fastmcp run -t http app/main.py & - run: sleep 5 - - run: curl http://127.0.0.1:8000/mcp - name: Run inspector - run: npx @modelcontextprotocol/inspector --cli \ http://127.0.0.1:8000/mcp --method tools/list + run: npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list opik: runs-on: ubuntu-latest steps: From cf0f1390df3363199f94dfcea6c11dda00855e2b Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 11:17:55 +0200 Subject: [PATCH 42/77] linting + cleaned up tests + added action --- .github/workflows/inspector.yml | 2 +- .../workflows/mcp-composite-action/action.yml | 25 +++++++ .github/workflows/tests.yml | 8 ++- app/mcp/v1/mcp.py | 4 +- tests/agent.py | 6 +- tests/e2e_tests.py | 8 +-- tests/import_tests.py | 0 tests/mcp-test/tools.py | 0 tests/mcptest.py | 51 -------------- tests/opik/tool_calls.py | 66 ++++--------------- tests/query_tests.py | 2 +- tests/tools_test.py | 2 +- 12 files changed, 54 insertions(+), 120 deletions(-) create mode 100644 .github/workflows/mcp-composite-action/action.yml delete mode 100644 tests/import_tests.py delete mode 100644 tests/mcp-test/tools.py delete mode 100644 tests/mcptest.py diff --git a/.github/workflows/inspector.yml b/.github/workflows/inspector.yml index e760d9f..89cc376 100644 --- a/.github/workflows/inspector.yml +++ b/.github/workflows/inspector.yml @@ -14,4 +14,4 @@ jobs: - run: bats -v - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - - run: npx @modelcontextprotocol/inspector --cli --config tests/mcp.json --server apple-stdio --method tools/list \ No newline at end of file + - run: npx @modelcontextprotocol/inspector --cli --config tests/mcp.json --server apple-stdio --method tools/list diff --git a/.github/workflows/mcp-composite-action/action.yml b/.github/workflows/mcp-composite-action/action.yml new file mode 100644 index 0000000..8b87eb4 --- /dev/null +++ b/.github/workflows/mcp-composite-action/action.yml @@ -0,0 +1,25 @@ +name: 'MCP server setup' +description: 'Sync uv dependencies and run MCP server' + +env: + DUCKDB_FILENAME: tests/parquet_example + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Install dependencies + run: uv sync --group dev + - name: Run fileserver + run: uv run --directory tests/ fileserver.py & + - name: Run mcp server + run: uv run fastmcp run -t http app/main.py & + - name: Wait for mcp initialization + run: sleep 5 \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f03c131..d0fb596 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,8 @@ jobs: run: uv run --directory tests/ fileserver.py & - name: Run mcp server run: uv run fastmcp run -t http app/main.py & - - run: sleep 5 + - name: Wait for mcp initialization + run: sleep 5 - name: Run tests run: uv run --directory tests/ pytest e2e_tests.py inspector: @@ -40,7 +41,8 @@ jobs: run: uv sync --group dev - name: Run mcp server run: uv run fastmcp run -t http app/main.py & - - run: sleep 5 + - name: Wait for mcp initialization + run: sleep 5 - name: Run inspector run: npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list opik: @@ -54,4 +56,4 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Run opik experiments - run: uv run tests/opik/tool_calls.py \ No newline at end of file + run: uv run tests/opik/tool_calls.py diff --git a/app/mcp/v1/mcp.py b/app/mcp/v1/mcp.py index cc72222..e7d99fc 100644 --- a/app/mcp/v1/mcp.py +++ b/app/mcp/v1/mcp.py @@ -1,10 +1,10 @@ from fastmcp import FastMCP -from app.mcp.v1.tools import ch_reader, duckdb_reader, es_reader, xml_reader +from app.mcp.v1.tools import duckdb_reader mcp_router = FastMCP(name="Main MCP") mcp_router.mount(duckdb_reader.duckdb_reader_router) # mcp_router.mount(ch_reader.ch_reader_router) # mcp_router.mount(es_reader.es_reader_router) -# mcp_router.mount(xml_reader.xml_reader_router) \ No newline at end of file +# mcp_router.mount(xml_reader.xml_reader_router) diff --git a/tests/agent.py b/tests/agent.py index 9fafffe..ce092e9 100644 --- a/tests/agent.py +++ b/tests/agent.py @@ -8,10 +8,10 @@ from pydantic_ai.mcp import MCPServerStreamableHTTP from dotenv import load_dotenv -load_dotenv() -from app.config import settings import opik +load_dotenv() + class AgentManager: def __init__(self): self.agent: Agent | None = None @@ -82,4 +82,4 @@ async def main(): if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/tests/e2e_tests.py b/tests/e2e_tests.py index 0f1196b..1d786bb 100644 --- a/tests/e2e_tests.py +++ b/tests/e2e_tests.py @@ -37,13 +37,13 @@ async def llm_opinion_template(query: str, expected: str): two inputs. Respond only with a percentage, e.g. "81", without % sign. Consider things like missing or differing data, you can ignore things like honorifics, your highest priority is data itself - + Input nr. 1: {output.parts[0].content} - + Input nr. 2: {expected} - + """) percent = int(resp) assert 75 <= percent <= 100 @@ -77,5 +77,3 @@ async def test_judge(): # @pytest.mark.asyncio # async def test_trend(): # await query_template("please give me trend data for my heart rate") - - diff --git a/tests/import_tests.py b/tests/import_tests.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/mcp-test/tools.py b/tests/mcp-test/tools.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/mcptest.py b/tests/mcptest.py deleted file mode 100644 index df392ea..0000000 --- a/tests/mcptest.py +++ /dev/null @@ -1,51 +0,0 @@ -# ruff: noqa -import asyncio - -import nest_asyncio -from agents import Agent, Runner -from agents.mcp import ( - MCPServerStreamableHttp, - MCPServerStreamableHttpParams, -) -from agents.model_settings import ModelSettings -from dotenv import load_dotenv - -load_dotenv() -nest_asyncio.apply() - - -async def main() -> None: - # params = MCPServerStdioParams( - # command="uv", - # args = [ - # "run", - # "--directory", - # "../app", - # "fastmcp", - # "run", - # "main.py" - # ] - # ) - # - # async with MCPServerStdio( - # name="apple serwer", - # params=params - # ) as server: - - params = MCPServerStreamableHttpParams( - url="http://localhost:8000/mcp/", - ) - - async with MCPServerStreamableHttp( - params=params, - ) as server: - agent = Agent( - name="cwel", - mcp_servers=[server], - model_settings=ModelSettings(tool_choice="required"), - ) - result = await Runner.run(agent, "give me a health summary from duckdb") - print(result.final_output) - - -asyncio.run(main()) diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index f96b51b..fe18fcf 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -19,13 +19,14 @@ class ToolSelectionQuality(base_metric.BaseMetric): def __init__(self, name: str = "tool_selection_quality"): - # super().__init__(name) + super().__init__(name) self.name = name def score(self, tool_calls, expected_tool_calls, **kwargs): try: actual_tool = tool_calls[0]["function_name"] expected_tool = expected_tool_calls[0]["function_name"] + if actual_tool == expected_tool: return score_result.ScoreResult( name=self.name, @@ -51,11 +52,12 @@ def evaluation_task(dataset_item): user_message_content = dataset_item["input"] expected_tool = dataset_item.get("tool_call", "") reference = dataset_item.get("expected_output", "") - # This is where you call your agent with the input message and get the real execution results. - resp = (agent_manager.agent.run_sync(user_message_content)) + + resp = agent_manager.agent.run_sync(user_message_content) result = resp.new_messages() tool_calls = [{"function_name": result[1].parts[0].tool_name, "function_parameters": {}}] + return { "input": user_message_content, "output": resp.output, @@ -74,67 +76,25 @@ def evaluation_task(dataset_item): } -metrics = [ToolSelectionQuality()] - client = opik.Opik() dataset = client.get_dataset(name="tool_calls") -dataset.insert([ - { - "input": "give me a summary of my health from duckdb", - "tool_call": "get_health_summary_duckdb" - }, - { - "input": "give me some statistics about my heart rate", - "tool_call": "get_statistics_by_type_duckdb" - }, - { - "input": "give me trend data for my step count in october 2024 from duckdb", - "tool_call": "get_trend_data_duckdb" - } -]) - judge_dataset = client.get_or_create_dataset(name="output_checks") -# judge_dataset.insert([ -# { -# "input": "give me a summary of my health from duckdb", -# "expected_output": """ -# Here is a summary of your health data from DuckDB: -# - **Basal Energy Burned**: 18 records - **Heart Rate**: -# 17 records - **Step Count**: 10 records - **Body Mass Index -# (BMI)**: 8 records - **Dietary Water**: 1 record If you -# need more detailed information or specific statistics -# on any of these categories, feel free to ask! -# """ -# }, -# { -# "input": "give me some statistics about my heart rate", -# "expected_output": """ -# idk yet -# """ -# }, -# { -# "input": "give me trend data for my step count in october 2024 from duckdb", -# "expected_output": """ -# idk yet -# """ -# } -# ]) eval_results = evaluate( experiment_name="AgentToolSelectionExperiment", dataset=dataset, task=evaluation_task, - scoring_metrics=metrics, + scoring_metrics=[ToolSelectionQuality()], task_threads=1, ) -# second_evals = evaluate( -# experiment_name="JudgeOutputExperiment", -# dataset=judge_dataset, -# task=evaluation_task, -# scoring_metrics=[Hallucination(), LevenshteinRatio(), AnswerRelevance(require_context=False)], -# task_threads=1, -# ) +second_evals = evaluate( + experiment_name="JudgeOutputExperiment", + dataset=judge_dataset, + task=evaluation_task, + scoring_metrics=[Hallucination(), LevenshteinRatio(), AnswerRelevance(require_context=False)], + task_threads=1, +) diff --git a/tests/query_tests.py b/tests/query_tests.py index 6af9df6..1e2e988 100644 --- a/tests/query_tests.py +++ b/tests/query_tests.py @@ -4,7 +4,7 @@ import pytest -path = Path(__file__).parent / "records.parquet" +path = Path(__file__).parent / "parquet_example" os.environ["DUCKDB_FILENAME"] = str(path) from app.schemas.record import HealthRecordSearchParams diff --git a/tests/tools_test.py b/tests/tools_test.py index 0da425f..e3cd842 100644 --- a/tests/tools_test.py +++ b/tests/tools_test.py @@ -19,4 +19,4 @@ async def get_mcp_tools(mcp: FastMCP): print(asyncio.run(get_mcp_tools(mcp_router))) -print(ad == ab) \ No newline at end of file +print(ad == ab) From 2eba7c9cb87777c150d1eb6e8483c3db6ae2669b Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 11:21:59 +0200 Subject: [PATCH 43/77] using composite action --- .../workflows/mcp-composite-action/action.yml | 2 +- .github/workflows/tests.yml | 24 ++----------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/.github/workflows/mcp-composite-action/action.yml b/.github/workflows/mcp-composite-action/action.yml index 8b87eb4..ce96cef 100644 --- a/.github/workflows/mcp-composite-action/action.yml +++ b/.github/workflows/mcp-composite-action/action.yml @@ -5,7 +5,7 @@ env: DUCKDB_FILENAME: tests/parquet_example OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} - + runs: using: "composite" steps: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d0fb596..ab7eb3e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,20 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Install dependencies - run: uv sync --group dev - - name: Run fileserver - run: uv run --directory tests/ fileserver.py & - - name: Run mcp server - run: uv run fastmcp run -t http app/main.py & - - name: Wait for mcp initialization - run: sleep 5 + - uses: ./.github/workflows/mcp-composite-action - name: Run tests run: uv run --directory tests/ pytest e2e_tests.py inspector: @@ -35,14 +22,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '20' - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Install dependencies - run: uv sync --group dev - - name: Run mcp server - run: uv run fastmcp run -t http app/main.py & - - name: Wait for mcp initialization - run: sleep 5 + - uses: ./.github/workflows/mcp-composite-action - name: Run inspector run: npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list opik: From 68b5e2285aba010f541e46bb265bf7daa7d86df6 Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 11:27:29 +0200 Subject: [PATCH 44/77] added shell and removed api keys --- .../workflows/mcp-composite-action/action.yml | 19 ++++++++++++++----- .github/workflows/tests.yml | 4 ++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/mcp-composite-action/action.yml b/.github/workflows/mcp-composite-action/action.yml index ce96cef..03d0b39 100644 --- a/.github/workflows/mcp-composite-action/action.yml +++ b/.github/workflows/mcp-composite-action/action.yml @@ -1,10 +1,12 @@ name: 'MCP server setup' description: 'Sync uv dependencies and run MCP server' -env: - DUCKDB_FILENAME: tests/parquet_example - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} + +inputs: + DUCKDB_FILENAME: + description: 'path to duckdb file' + required: false + default: 'tests/parquet_example' runs: using: "composite" @@ -15,11 +17,18 @@ runs: python-version: '3.13' - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + shell: bash - name: Install dependencies run: uv sync --group dev + shell: bash - name: Run fileserver run: uv run --directory tests/ fileserver.py & + shell: bash - name: Run mcp server run: uv run fastmcp run -t http app/main.py & + env: + DUCKDB_FILENAME: ${{ inputs.DUCKDB_FILENAME }} + shell: bash - name: Wait for mcp initialization - run: sleep 5 \ No newline at end of file + run: sleep 5 + shell: bash \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ab7eb3e..9b90010 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/mcp-composite-action + with: + DUCKDB_FILENAME: 'tests/parquet_example' - name: Run tests run: uv run --directory tests/ pytest e2e_tests.py inspector: @@ -23,6 +25,8 @@ jobs: with: node-version: '20' - uses: ./.github/workflows/mcp-composite-action + with: + DUCKDB_FILENAME: 'tests/parquet_example' - name: Run inspector run: npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list opik: From 44577099c0239af6a8d066a68573865a2055ce1c Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 11:30:21 +0200 Subject: [PATCH 45/77] added mcp composite to opik tests --- .github/workflows/tests.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b90010..6b0a0bb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,11 +33,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: Set up Python - uses: actions/setup-python@v5 + - uses: ./.github/workflows/mcp-composite-action with: - python-version: '3.13' - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh + DUCKDB_FILENAME: 'tests/parquet_example' - name: Run opik experiments run: uv run tests/opik/tool_calls.py From bb99d6c89fa56031ee8940f20cc1b11d0f917d9e Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 11:42:59 +0200 Subject: [PATCH 46/77] debug --- tests/opik/tool_calls.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index fe18fcf..a35f44b 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -80,7 +80,7 @@ def evaluation_task(dataset_item): dataset = client.get_dataset(name="tool_calls") -judge_dataset = client.get_or_create_dataset(name="output_checks") +judge_dataset = client.get_dataset(name="output_checks") eval_results = evaluate( @@ -91,6 +91,13 @@ def evaluation_task(dataset_item): task_threads=1, ) +import requests as req + +r1 = req.get("http://localhost:8000/mcp") +print(r1.text) +r2 = req.get("https://www.comet.com/opik/api/v1/private/spans/batch") +print(r2.text) + second_evals = evaluate( experiment_name="JudgeOutputExperiment", dataset=judge_dataset, From b9c1bb537ea66b21b02ba9dd1c42f32aa97fa79c Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 11:46:27 +0200 Subject: [PATCH 47/77] more debug --- tests/opik/tool_calls.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index a35f44b..f96fed1 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -98,6 +98,9 @@ def evaluation_task(dataset_item): r2 = req.get("https://www.comet.com/opik/api/v1/private/spans/batch") print(r2.text) +print(eval_results.test_results) +print(eval_results.experiment_name) + second_evals = evaluate( experiment_name="JudgeOutputExperiment", dataset=judge_dataset, @@ -105,3 +108,6 @@ def evaluation_task(dataset_item): scoring_metrics=[Hallucination(), LevenshteinRatio(), AnswerRelevance(require_context=False)], task_threads=1, ) + +print(second_evals.test_results) +print(second_evals.experiment_name) \ No newline at end of file From 3face7129ec37563de08b861b732595587aa4e6b Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 11:58:36 +0200 Subject: [PATCH 48/77] github guy fix --- .github/workflows/tests.yml | 1 + tests/opik/tool_calls.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6b0a0bb..4911d4d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,6 +6,7 @@ env: DUCKDB_FILENAME: tests/parquet_example OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} + OPIK_WORKSPACE: Default Project jobs: test: diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index f96fed1..baa0e09 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -1,6 +1,7 @@ import asyncio +import os -import opik +from opik import Opik from opik.evaluation import evaluate from opik.evaluation.metrics import ( base_metric, @@ -75,8 +76,19 @@ def evaluation_task(dataset_item): "error": str(e) } +# refer to https://github.com/comet-ml/opik/issues/2118 -client = opik.Opik() +opik_workspace = os.getenv("OPIK_WORKSPACE_NAME") +opik_api_key = os.getenv("OPIK_API_KEY") + +os.environ["OPIK_WORKSPACE"] = opik_workspace +os.environ["OPIK_API_KEY"] = opik_api_key + +client = Opik( + # use_local=False, # Set to True if using a local Opik instance + workspace=opik_workspace, + api_key=opik_api_key +) dataset = client.get_dataset(name="tool_calls") From 11287913f6c9c1f77580d7a1d1e333fde3e904f1 Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 12:00:49 +0200 Subject: [PATCH 49/77] name fix typo --- .github/workflows/tests.yml | 2 +- tests/opik/tool_calls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4911d4d..8de8c41 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,7 +6,7 @@ env: DUCKDB_FILENAME: tests/parquet_example OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} - OPIK_WORKSPACE: Default Project + OPIK_WORKSPACE: 'Default Project' jobs: test: diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index baa0e09..a7191fd 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -78,7 +78,7 @@ def evaluation_task(dataset_item): # refer to https://github.com/comet-ml/opik/issues/2118 -opik_workspace = os.getenv("OPIK_WORKSPACE_NAME") +opik_workspace = os.getenv("OPIK_WORKSPACE") opik_api_key = os.getenv("OPIK_API_KEY") os.environ["OPIK_WORKSPACE"] = opik_workspace From 7ff2d574ad595eed04e1e2d961bfca05d7f4660a Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 12:05:25 +0200 Subject: [PATCH 50/77] workspace name fix --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8de8c41..5e3e39e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,7 +6,7 @@ env: DUCKDB_FILENAME: tests/parquet_example OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} - OPIK_WORKSPACE: 'Default Project' + OPIK_WORKSPACE: 'czajkub' jobs: test: From db279bca1102d4db8c0c3252df4cdc0cd1815196 Mon Sep 17 00:00:00 2001 From: czajkub Date: Mon, 29 Sep 2025 12:30:51 +0200 Subject: [PATCH 51/77] cleanup & linting --- .github/workflows/inspector.yml | 17 ------------ .../workflows/mcp-composite-action/action.yml | 2 +- .github/workflows/tests.yml | 2 +- .pre-commit-config.yaml | 5 ++++ pyproject.toml | 6 ++--- tests/agent.py | 6 +---- tests/mcp.json | 27 ------------------- tests/opik/tool_calls.py | 17 ++---------- tests/tools_test.py | 22 --------------- 9 files changed, 13 insertions(+), 91 deletions(-) delete mode 100644 .github/workflows/inspector.yml delete mode 100644 tests/mcp.json delete mode 100644 tests/tools_test.py diff --git a/.github/workflows/inspector.yml b/.github/workflows/inspector.yml deleted file mode 100644 index 89cc376..0000000 --- a/.github/workflows/inspector.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: kijjhjvjm-github-actions - -on: [push] - -jobs: - check-bats-version: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 - with: - node-version: '20' - - run: npm install -g bats - - run: bats -v - - name: Install uv - run: curl -LsSf https://astral.sh/uv/install.sh | sh - - run: npx @modelcontextprotocol/inspector --cli --config tests/mcp.json --server apple-stdio --method tools/list diff --git a/.github/workflows/mcp-composite-action/action.yml b/.github/workflows/mcp-composite-action/action.yml index 03d0b39..2d7a641 100644 --- a/.github/workflows/mcp-composite-action/action.yml +++ b/.github/workflows/mcp-composite-action/action.yml @@ -31,4 +31,4 @@ runs: shell: bash - name: Wait for mcp initialization run: sleep 5 - shell: bash \ No newline at end of file + shell: bash diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5e3e39e..4a9b15e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,7 +6,7 @@ env: DUCKDB_FILENAME: tests/parquet_example OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} - OPIK_WORKSPACE: 'czajkub' + OPIK_WORKSPACE: ${{ secrets.OPIK_WORKSPACE }} jobs: test: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9687dbf..ff1a78c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,3 +20,8 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer + +exclude: | + (?x)( + ^tests/ + ) diff --git a/pyproject.toml b/pyproject.toml index 478d0db..1716005 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,9 @@ code-quality = [ [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "session" +[tool.ty.src] +exclude = ["./tests/"] + [tool.ruff] line-length = 100 target-version = "py313" @@ -76,8 +79,5 @@ ignore = [ "ANN401", # any-type ] -[tool.ty.src] -exclude = ["tests/"] - [project.scripts] start = "start:main" diff --git a/tests/agent.py b/tests/agent.py index ce092e9..dc9a8f5 100644 --- a/tests/agent.py +++ b/tests/agent.py @@ -57,10 +57,6 @@ async def handle_message(self, message: str) -> str: def is_initialized(self) -> bool: return self._initialized - async def close(self): - """Close the MCP client""" - if self.mcp_client: - await self.mcp_client.close() agent_manager = AgentManager() @@ -78,7 +74,7 @@ async def main(): response = await agent_manager.handle_message(user_input) print("Agent: ", response) finally: - await agent_manager.close() + print("Closing agent") if __name__ == "__main__": diff --git a/tests/mcp.json b/tests/mcp.json deleted file mode 100644 index 25e10db..0000000 --- a/tests/mcp.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "mcpServers": { - "apple": { - "type": "streamable-http", - "url": "http://localhost:8000/mcp", - "note": "For Streamable HTTP connections, add this URL directly in your MCP Client", - "env": { - "DUCKDB_FILENAME": "/mnt/c/Users/czajk/Desktop/apple/" - } - }, - "apple-stdio": { - "type": "stdio", - "command": "uv", - "args": [ - "run", - "--directory", - "../", - "fastmcp", - "run", - "app/main.py" - ], - "env": { - "DUCKDB_FILENAME": "/mnt/c/Users/czajk/Desktop/apple/" - } - } - } -} diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index a7191fd..56e5ee6 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -85,7 +85,6 @@ def evaluation_task(dataset_item): os.environ["OPIK_API_KEY"] = opik_api_key client = Opik( - # use_local=False, # Set to True if using a local Opik instance workspace=opik_workspace, api_key=opik_api_key ) @@ -95,7 +94,7 @@ def evaluation_task(dataset_item): judge_dataset = client.get_dataset(name="output_checks") -eval_results = evaluate( +tool_call_evals = evaluate( experiment_name="AgentToolSelectionExperiment", dataset=dataset, task=evaluation_task, @@ -103,23 +102,11 @@ def evaluation_task(dataset_item): task_threads=1, ) -import requests as req -r1 = req.get("http://localhost:8000/mcp") -print(r1.text) -r2 = req.get("https://www.comet.com/opik/api/v1/private/spans/batch") -print(r2.text) - -print(eval_results.test_results) -print(eval_results.experiment_name) - -second_evals = evaluate( +output_test_evals = evaluate( experiment_name="JudgeOutputExperiment", dataset=judge_dataset, task=evaluation_task, scoring_metrics=[Hallucination(), LevenshteinRatio(), AnswerRelevance(require_context=False)], task_threads=1, ) - -print(second_evals.test_results) -print(second_evals.experiment_name) \ No newline at end of file diff --git a/tests/tools_test.py b/tests/tools_test.py deleted file mode 100644 index e3cd842..0000000 --- a/tests/tools_test.py +++ /dev/null @@ -1,22 +0,0 @@ -import asyncio - -from fastmcp import FastMCP - -from app.mcp.v1.mcp import mcp_router - - -app = FastMCP() - -@app.tool -def add_nr(a: int, b: int) -> int: - return a + b - -async def get_mcp_tools(mcp: FastMCP): - return await mcp.get_tools() - -ad = {"b": 6, "a": 5, "6": 7} -ab = {"6": 7, "a": 5, "b": 6} - -print(asyncio.run(get_mcp_tools(mcp_router))) - -print(ad == ab) From 55525e1a9a6df56ee05526a8e7e2c6f74e48ee31 Mon Sep 17 00:00:00 2001 From: czajkub Date: Tue, 30 Sep 2025 16:08:33 +0200 Subject: [PATCH 52/77] split readme --- README.md | 277 ++++++------------------------------------------------ 1 file changed, 28 insertions(+), 249 deletions(-) diff --git a/README.md b/README.md index c8590b3..3886eb4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

Apple Health MCP Server

-

Apple Health Data Management

+

Apple Health Data Exploration

[![Contact us](https://img.shields.io/badge/Contact%20us-AFF476.svg?style=for-the-badge&logo=mail&logoColor=black)](mailto:hello@themomentum.ai?subject=Apple%20Health%20MCP%20Server%20Inquiry) [![Visit Momentum](https://img.shields.io/badge/Visit%20Momentum-1f6ff9.svg?style=for-the-badge&logo=safari&logoColor=white)](https://themomentum.ai) @@ -14,48 +14,8 @@
-## πŸ“‹ Table of Contents - -- [πŸ” About](#-about-the-project) -- [πŸ’‘ Demo](#-demo) -- [πŸš€ Getting Started](#-getting-started) -- [πŸ“ Usage](#-usage) -- [πŸ”§ Configuration](#-configuration) -- [🐳 Docker Setup](#-docker-mcp) -- [πŸ› οΈ MCP Tools](#️-mcp-tools) -- [πŸ—ΊοΈ Roadmap](#️-roadmap) -- [πŸ‘₯ Contributors](#-contributors) -- [πŸ“„ License](#-license) - -## πŸ” About The Project - -**Apple Health MCP Server** implements a Model Context Protocol (MCP) server designed for seamless interaction between LLM-based agents and Apple Health data. It provides a standardized interface for querying, analyzing, and managing Apple Health recordsβ€”imported from XML exports and indexed in Elasticsearch, Clickhouse or DuckDBβ€”through a comprehensive suite of tools. These tools are accessible from MCP-compatible clients (such as Claude Desktop), enabling users to explore, search, and analyze personal health data using natural-language prompts and advanced filtering, all without requiring direct knowledge of the underlying data formats or Elasticsearch/ClickHouse/DuckDB queries. - -### ✨ Key Features - -- **πŸš€ FastMCP Framework**: Built on FastMCP for high-performance MCP server capabilities -- **🍏 Apple Health Data Management**: Import, parse, and analyze Apple Health XML exports -- **πŸ”Ž Powerful Search & Filtering**: Query and filter health records using natural language and advanced parameters -- **πŸ“¦ Elasticsearch, ClickHouse or DuckDB Integration**: Index and search health data efficiently at scale -- **πŸ› οΈ Modular MCP Tools**: Tools for structure analysis, record search, type-based extraction, and more -- **πŸ“ˆ Data Summaries & Trends**: Generate statistics and trend analyses from your health data -- **🐳 Container Ready**: Docker support for easy deployment and scaling -- **πŸ”§ Configurable**: Extensive ```.env```-based configuration options - -### πŸ—οΈ Architecture - -The Apple Health MCP Server is built with a modular, extensible architecture designed for robust health data management and LLM integration: - -- **MCP Tools**: Dedicated tools for Apple Health XML structure analysis, record search, type-based extraction, and statistics/trend generation. Each tool is accessible via the MCP protocol for natural language and programmatic access. -- **XML Import & Parsing**: Efficient streaming and parsing of large Apple Health XML exports, extracting records, workouts, and metadata for further analysis. -- **Elasticsearch Backend**: All health records are indexed in Elasticsearch, enabling fast, scalable search, filtering, and aggregation across large datasets. -- **ClickHouse Backend**: Health records can also be indexed to a ClickHouse database, making the deployment easier for the enduser by using an in-memory database instead of a server-based approach. -- **DuckDB Backend**: Alternative to both Elasticsearch and ClickHouse, DuckDB may offer faster import and query speeds. -- **Service Layer**: Business logic for XML and database operations is encapsulated in dedicated service modules, ensuring separation of concerns and easy extensibility. -- **FastMCP Framework**: Provides the MCP server interface, routing, and tool registration, making the system compatible with LLM-based agents and MCP clients (e.g., Claude Desktop). -- **Configuration & Deployment**: Environment-based configuration and Docker support for easy setup and deployment in various environments. - -

(back to top)

+--- +Connect your Apple Health data with any LLM that supports MCP. Talk to your data and get personalised insights. ## πŸ’‘ Demo @@ -66,216 +26,35 @@ This demo shows how Claude uses the `apple-health-mcp-server` to answer question https://github.com/user-attachments/assets/93ddbfb9-6da9-42c1-9872-815abce7e918 -## πŸš€ Getting Started - -Follow these steps to set up Apple Health MCP Server in your environment. - -### Prerequisites - -- **Docker (recommended) or uv + docker**: For dependency management - - πŸ‘‰ [uv Installation Guide](https://docs.astral.sh/uv/getting-started/installation/) -- **Clone the repository**: - ```sh - git clone https://github.com/the-momentum/apple-health-mcp-server - cd apple-health-mcp-server - ``` - -- **Set up environment variables**: - ```sh - cp config/.env.example config/.env - ``` - Edit the `config/.env` file with your credentials and configuration. See [Environment Variables](#-Environment-Variables) - -### Prepare Your Data - -1. Export your Apple Health data as an XML file from your iPhone and place it somewhere in your filesystem. By default, the server expects the file in the project root directory. - - if you need working example, we suggest this dataset: https://drive.google.com/file/d/1bWiWmlqFkM3MxJZUD2yAsNHlYrHvCmcZ/view?usp=drive_link - - Rob Mulla. Predict My Sleep Patterns. https://kaggle.com/competitions/kaggle-pog-series-s01e04, 2023. Kaggle. -2. Prepare an Elasticsearch instance and populate it from the XML file: - - Run `make es` to start Elasticsearch and import your XML data. - - (Optional) To clear all data from the Elasticsearch index, run: - ```sh - uv run python scripts/xml2es.py --delete-all - ``` -3. If you choose to use ClickHouse instead of Elasticsearch: - - Run `make ch` to create a database with your exported XML data - - **Note: If you are using Windows, Docker is the only way to integrate ClickHouse into this MCP Server.** - - On Windows: Run `mingw32-make chwin` (or any other version of `make` available on Windows) - -4. Lastly, if you're going to be using DuckDB: - - Run `make duckdb` to create a parquet file with your exported XML data - - If you want to connect to the file through http(s): - - The only thing you need to do is change the .env path, e.g. `localhost:8080/applehealth.parquet` - - If you want an example on how to host the files locally, run `uv run tests/fileserver.py` - - -### Configuration Files - -You can run the MCP Server in your LLM Client in two ways: -- **Docker** (recommended) -- **Local (uv run)** - -#### Docker MCP Server - -1. Build the Docker image: - ```sh - make build - ``` -2. Add the following config to your LLM Client settings (replace `` with your local repository path and `` with name of your raw data from apple health file (without `.xml` extension)): - ```json - { - "mcpServers": { - "docker-mcp-server": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "--init", - "--mount", - "type=bind,source=/{xml-file-name}.xml,target=/root_project/raw.xml", - "--mount", // optional - volume for reload - "type=bind,source=/app,target=/root_project/app", // optional - "--mount", - "type=bind,source=/config/.env,target=/root_project/config/.env", - "--mount", // optional - only include this if you use clickhouse - "type=bind,source=/applehealth.chdb,target=/root_project/applehealth.chdb", // optional - "--mount", // optional - only include this if you use duckdb - "type=bind,source=/,target=/root_project/applehealth.parquet", // optional - "-e", - "ES_HOST=host.docker.internal", - "mcp-server:latest" - ] - } - } - } - ``` - -#### Local uv MCP Server -1. Get the path to your `uv` binary: - - On Windows: - ```powershell - (Get-Command uv).Path - ``` - - On MacOS/Linux: - ```sh - which uv - ``` -2. Add the following config to your LLM Client settings (replace `` and `` as appropriate): - ```json - { - "mcpServers": { - "uv-mcp-server": { - "command": "uv", - "args": [ - "run", - "--frozen", - "--directory", - "", - "start" - ], - "env": { - "PATH": "" - } - } - } - } - ``` - - `` should be the folder containing the `uv` binary (do not include `uv` itself at the end). +Want to try it out? **[πŸš€ Getting Started](docs/getting-started.md)** -### 3. Restart Your MCP Client +## 🌟 Why to use Apple Health MCP Server? -After completing the above steps, restart your MCP Client to apply the changes. In some cases, you may need to terminate all related processes using Task Manager or your system's process manager to ensure: -- The updated configuration is properly loaded -- Environment variables are correctly applied -- The Apple Health MCP client initializes with the correct settings + - **🧩 Fit your data everywhere**: using this software you can import data exported from Apple devices into any DBMS, base importer is already prepared for extensions + - **🎯 Simplify complex data access**: you don't need to know data structure or use any structured query language, like SQL, simple access is just granted with natural language + - **πŸ”οΈŽ Find hidden trends**: use LLM as a gate to flexible auto-generated queries which will be able to find data trends not so easy to detect manually -

(back to top)

- - -## πŸ”§ Configuration - -### Environment Variables - -> **Note:** All variables below are optional unless marked as required. If not set, the server will use the default values shown. Only `RAW_XML_PATH` is required and must point to your Apple Health XML file. - -| Variable | Description | Example Value | Required | -|--------------------|--------------------------------------------|----------------------|----------| -| RAW_XML_PATH | Path to the Apple Health XML file | `raw.xml` | βœ… | -| ES_HOST | Elasticsearch host | `localhost` | ❌ | -| ES_PORT | Elasticsearch port | `9200` | ❌ | -| ES_USER | Elasticsearch username | `elastic` | ❌ | -| ES_PASSWORD | Elasticsearch password | `elastic` | ❌ | -| ES_INDEX | Elasticsearch index name | `apple_health_data` | ❌ | -| CH_DIRNAME | ClickHouse directory name | `applehealth.chdb` | ❌ | -| CH_DB_NAME | ClickHouse database name | `applehealth` | ❌ | -| CH_TABLE_NAME | ClickHouse table name | `data` | ❌ | -| DUCKDB_FILENAME | DuckDB parquet file name | `applehealth` | ❌ | -| CHUNK_SIZE | Records indexed into CH/DuckDB at once | `50000` | ❌ | -| XML_SAMPLE_SIZE | Number of XML records to sample | `1000` | ❌ | - -

(back to top)

- - -## πŸ› οΈ MCP Tools - -The Apple Health MCP Server provides a suite of tools for exploring, searching, and analyzing your Apple Health data, both at the raw XML level and in Elasticsearch/ClickHouse: - -### XML Tools (`xml_reader`) - -| Tool | Description | -|---------------------|-----------------------------------------------------------------------------------------------| -| `get_xml_structure` | Analyze the structure and metadata of your Apple Health XML export (file size, tags, types). | -| `search_xml_content`| Search for specific content in the XML file (by attribute value, device, type, etc.). | -| `get_xml_by_type` | Extract all records of a specific health record type from the XML file. | - -### Elasticsearch Tools (`es_reader`) - -| Tool | Description | -|-----------------------------|-----------------------------------------------------------------------------------------------------| -| `get_health_summary_es` | Get a summary of all Apple Health data in Elasticsearch (total count, type breakdown, etc.). | -| `search_health_records_es` | Flexible search for health records in Elasticsearch with advanced filtering and query options. | -| `get_statistics_by_type_es` | Get comprehensive statistics (count, min, max, avg, sum) for a specific health record type. | -| `get_trend_data_es` | Analyze trends for a health record type over time (daily, weekly, monthly, yearly aggregations). | -| `search_values_es` | Search for records with exactly matching values (including text). | - -### ClickHouse Tools (`ch_reader`) +## ✨ Key Features -| Tool | Description | -|-----------------------------|-----------------------------------------------------------------------------------------------------| -| `get_health_summary_ch` | Get a summary of all Apple Health data in ClickHouse (total count, type breakdown, etc.). | -| `search_health_records_ch` | Flexible search for health records in ClickHouse with advanced filtering and query options. | -| `get_statistics_by_type_ch` | Get comprehensive statistics (count, min, max, avg, sum) for a specific health record type. | -| `get_trend_data_ch` | Analyze trends for a health record type over time (daily, weekly, monthly, yearly aggregations). | -| `search_values_ch` | Search for records with exactly matching values (including text). | - -### DuckDB Tools (`duckdb_reader`) - -| Tool | Description | -|-----------------------------|-----------------------------------------------------------------------------------------------------| -| `get_health_summary_duckdb` | Get a summary of all Apple Health data in DuckDB (total count, type breakdown, etc.). | -| `search_health_records_duckdb` | Flexible search for health records in DuckDB with advanced filtering and query options. | -| `get_statistics_by_type_duckdb` | Get comprehensive statistics (count, min, max, avg, sum) for a specific health record type. | -| `get_trend_data_duckdb` | Analyze trends for a health record type over time (daily, weekly, monthly, yearly aggregations). | -| `search_values_duckdb` | Search for records with exactly matching values (including text). | - -All tools are accessible via MCP-compatible clients and can be used with natural language or programmatic queries to explore and analyze your Apple Health data. - -

(back to top)

- - -## πŸ—ΊοΈ Roadmap +- **πŸš€ FastMCP Framework**: Built on FastMCP for high-performance MCP server capabilities +- **🍏 Apple Health Data Exploration**: Import, parse, and analyze Apple Health XML exports +- **πŸ”Ž Powerful Search & Filtering**: Query and filter health records using natural language and advanced parameters +- **πŸ“¦ Elasticsearch, ClickHouse or DuckDB Integration**: Index and search health data efficiently at scale +- **πŸ› οΈ Modular MCP Tools**: Tools for structure analysis, record search, type-based extraction, and more +- **πŸ“ˆ Data Summaries & Trends**: Generate statistics and trend analyses from your health data +- **🐳 Container Ready**: Docker support for easy deployment and scaling +- **πŸ”§ Configurable**: Extensive ```.env```-based configuration options -We're continuously enhancing Apple Health MCP Server with new capabilities. Here's what's on the horizon: +## πŸ“š Documentation -- [ ] **Time Series Sampling During Import**: Add advanced analytical tools to sample and generate time series data directly during the XML-to-Elasticsearch loading process. -- [ ] **Optimized XML Tools**: Improve the performance and efficiency of XML parsing and querying tools. -- [ ] **Expanded Elasticsearch Analytics**: Add more advanced analytics and aggregation functions to the Elasticsearch toolset. -- [ ] **Embedded Database Tools**: Integrate tools for working with embedded databases for local/offline analytics and storage. +- **[πŸš€ Getting Started](docs/getting-started.md)** - Complete setup guide +- **[πŸ” About](docs/about.md)** - Detailed description & architecture +- **[πŸ”§ Configuration](docs/configuration.md)** - Environment variables and settings +- **[πŸ› οΈ MCP Tools](docs/mcp-tools.md)** - All available tools +- **[πŸ—ΊοΈ Roadmap](docs/roadmap.md)** - Upcoming features and roadmap -Have a suggestion? We'd love to hear from you! Contact us or contribute directly. +**Need help?** Looking for guidance on use cases or implementation? Don't hesitate to ask your question in our [GitHub discussion forum](https://github.com/the-momentum/apple-health-mcp-server/discussions)! You'll also find interesting use cases, tips, and community insights there. ## πŸ‘₯ Contributors @@ -285,11 +64,11 @@ Have a suggestion? We'd love to hear from you! Contact us or contribute directly

(back to top)

-## πŸ“„ License +## πŸ’Ό About Momentum +This project is part of Momentum’s open-source ecosystem, where we make healthcare technology more secure, interoperable, and AI-ready. Our goal is to help HealthTech teams adopt standards such as FHIR safely and efficiently. We are healthcare AI development experts, recognized by FT1000, Deloitte Fast 50, and Forbes for building scalable, HIPAA-compliant solutions that power next-generation healthcare innovation. -Distributed under the MIT License. See [MIT License](LICENSE) for more information. - ---- +πŸ“– Want to learn from our experience? Read our insights β†’ themomentum.ai/blog. +Interested? Let's talk!

Built with ❀️ by Momentum β€’ Transforming healthcare data management with AI

From 243955a151325e5c001d719d361843356f4549a4 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 1 Oct 2025 11:25:17 +0200 Subject: [PATCH 53/77] removing debug code & linting --- README.md | 2 +- app/services/health/duckdb_queries.py | 22 ++++-------------- pyproject.toml | 2 -- scripts/duckdb_importer.py | 14 ------------ scripts/xml_exporter.py | 7 +++--- uv.lock | 32 +-------------------------- 6 files changed, 9 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 3886eb4..3b50df4 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Want to try it out? **[πŸš€ Getting Started](docs/getting-started.md)** ## πŸ’Ό About Momentum This project is part of Momentum’s open-source ecosystem, where we make healthcare technology more secure, interoperable, and AI-ready. Our goal is to help HealthTech teams adopt standards such as FHIR safely and efficiently. We are healthcare AI development experts, recognized by FT1000, Deloitte Fast 50, and Forbes for building scalable, HIPAA-compliant solutions that power next-generation healthcare innovation. -πŸ“– Want to learn from our experience? Read our insights β†’ themomentum.ai/blog. +πŸ“– Want to learn from our experience? Read our insights β†’ themomentum.ai/blog. Interested? Let's talk!
diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index e15d63e..c283638 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -1,7 +1,6 @@ from typing import Any import duckdb -from fastmcp.server.dependencies import get_context from app.schemas.record import HealthRecordSearchParams, IntervalType, RecordType from app.services.duckdb_client import DuckDBClient @@ -11,19 +10,10 @@ def get_health_summary_from_duckdb() -> list[dict[str, Any]]: - try: - response = duckdb.sql( - f"""SELECT type, COUNT(*) AS count FROM read_parquet('{client.path}') - GROUP BY type ORDER BY count DESC""", - ) - except duckdb.IOException: - try: - ctx = get_context() - ctx.error("Failed to connect to DuckDB") - except RuntimeError: - print("Failed to connect to DuckDB") - return [{"status_code": 400, "error": "failed to connect to DuckDB", "path": client.path}] - + response = duckdb.sql( + f"""SELECT type, COUNT(*) AS count FROM read_parquet('{client.path}') + GROUP BY type ORDER BY count DESC""", + ) return client.format_response(response) @@ -80,7 +70,3 @@ def search_values_from_duckdb( {f"AND startDate <= '{date_to}'" if date_to else ""} """) return client.format_response(result) - - -if __name__ == "__main__": - print(get_health_summary_from_duckdb()) diff --git a/pyproject.toml b/pyproject.toml index 1716005..fdcbc95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,6 @@ module-name = "app" [dependency-groups] dev = [ "fastapi>=0.116.2", - "nest-asyncio>=1.6.0", - "openai-agents>=0.3.2", "opik>=1.8.56", "pydantic-ai>=1.0.10", "pytest>=8.4.1", diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index 68f9ff4..80ec8a8 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -1,7 +1,6 @@ import os from pathlib import Path -import duckdb import polars as pl from app.services.duckdb_client import DuckDBClient @@ -88,19 +87,6 @@ def export_xml(self) -> None: for f in self.chunk_files: os.remove(f) - def export_to_multiple(self) -> None: - con = duckdb.connect("shitass.duckdb") - for i, docs in enumerate(self.parse_xml(), 1): - tables = docs.partition_by("type", as_dict=True) - for key, table in tables.items(): - pddf = table.to_pandas() # noqa - con.execute(f""" - CREATE TABLE IF NOT EXISTS {key[0]} - AS SELECT * FROM pddf - """) - # print(duckdb.sql(f"select * from pddf").fetchall()) - print(f"processed {i * self.chunk_size} docs") - if __name__ == "__main__": importer = ParquetImporter() diff --git a/scripts/xml_exporter.py b/scripts/xml_exporter.py index d6f7855..b9a89de 100644 --- a/scripts/xml_exporter.py +++ b/scripts/xml_exporter.py @@ -50,7 +50,7 @@ def __init__(self): "unit", ) - def update_record(self, kind: str, document: dict[str, Any]) -> dict[str, Any]: + def update_record(self, table: str, document: dict[str, Any]) -> dict[str, Any]: """ Updates records to fill out columns without specified data: There are 9 columns that need to be filled out, and there are 4 columns @@ -61,7 +61,7 @@ def update_record(self, kind: str, document: dict[str, Any]) -> dict[str, Any]: if field in document: document[field] = datetime.strptime(document[field], "%Y-%m-%d %H:%M:%S %z") - if kind == "record": + if table == "record": if len(document) != 9: document.update({k: v for k, v in self.DEFAULT_VALUES.items() if k not in document}) @@ -72,7 +72,7 @@ def update_record(self, kind: str, document: dict[str, Any]) -> dict[str, Any]: except (TypeError, ValueError): document["value"] = 0.0 - elif kind == "workout": + elif table == "workout": document["type"] = document.pop("workoutActivityType") try: @@ -124,7 +124,6 @@ def parse_xml(self) -> Generator[pl.DataFrame, Any, None]: elem.clear() # yield remaining records - # yield pl.DataFrame(records) yield DataFrame(records).reindex(columns=self.RECORD_COLUMNS) yield DataFrame(workouts).reindex(columns=self.WORKOUT_COLUMNS) yield DataFrame(workout_stats).reindex(columns=self.WORKOUT_STATS_COLUMNS) diff --git a/uv.lock b/uv.lock index 52773cf..2bf2b61 100644 --- a/uv.lock +++ b/uv.lock @@ -139,8 +139,6 @@ code-quality = [ ] dev = [ { name = "fastapi" }, - { name = "nest-asyncio" }, - { name = "openai-agents" }, { name = "opik" }, { name = "pydantic-ai" }, { name = "pytest" }, @@ -174,8 +172,6 @@ code-quality = [ ] dev = [ { name = "fastapi", specifier = ">=0.116.2" }, - { name = "nest-asyncio", specifier = ">=1.6.0" }, - { name = "openai-agents", specifier = ">=0.3.2" }, { name = "opik", specifier = ">=1.8.56" }, { name = "pydantic-ai", specifier = ">=1.0.10" }, { name = "pytest", specifier = ">=8.4.1" }, @@ -733,6 +729,7 @@ wheels = [ name = "fastuuid" version = "0.13.3" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/00/5ab21d121752b4efe57b0c4ae0b84161261779631bc4c76132c77cce97bb/fastuuid-0.13.3.tar.gz", hash = "sha256:d755dc132addb369877429622535dfb52d79c8968a8a98f15a84c3da1db013c7", size = 18249, upload-time = "2025-09-26T08:50:50.782Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/41/9482a375d3af33e2cdb99d3fa1bbbdb95f2e698ceb29e38880f81d919ebe/fastuuid-0.13.3-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e5be92899120006ed44b263c02588d38632b49aa1fb2a8fcd18bb5b93a1fa7f2", size = 494635, upload-time = "2025-09-25T17:18:44.961Z" }, { url = "https://files.pythonhosted.org/packages/46/bf/50530bb9bcc505ea74c06ac376af44c2b4e085a897b2d4fb168729267418/fastuuid-0.13.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:217c8438e4b2d727c810ff4c49123e45f2109925b04463745e382c7d808472fa", size = 253079, upload-time = "2025-09-25T17:18:19.086Z" }, @@ -1400,15 +1397,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/02/9d3b881bee5552600c6f456e446069d5beffd2b7862b99e1e945d60d6a9b/mypy_boto3_bedrock_runtime-1.40.21-py3-none-any.whl", hash = "sha256:4c9ea181ef00cb3d15f9b051a50e3b78272122d24cd24ac34938efe6ddfecc62", size = 34149, upload-time = "2025-08-29T19:25:03.941Z" }, ] -[[package]] -name = "nest-asyncio" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, -] - [[package]] name = "nexus-rpc" version = "1.1.0" @@ -1501,24 +1489,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/d3/d65e318e8d3b5845afaa5960d8da779988b4cb9f4e1ca82e588fed9c6a9d/openai-1.109.0-py3-none-any.whl", hash = "sha256:8c0910bdd4ee1274d5ff0354786bdd0bc79e68c158d5d2c19e24208b412e5792", size = 948421, upload-time = "2025-09-23T16:59:39.516Z" }, ] -[[package]] -name = "openai-agents" -version = "0.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffe" }, - { name = "mcp" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "types-requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/9f/dafa9f80653778179822e1abf77c7f0d9da5a16806c96b5bb9e0e46bd747/openai_agents-0.3.2.tar.gz", hash = "sha256:b71ac04ee9f502f1bc0f4d142407df4ec69db4442db86c4da252b4558fa90cd5", size = 1727988, upload-time = "2025-09-23T20:37:20.7Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/7e/6a8437f9f40937bb473ceb120a65e1b37bc87bcee6da67be4c05b25c6a89/openai_agents-0.3.2-py3-none-any.whl", hash = "sha256:55e02c57f2aaf3170ff0aa0ab7c337c28fd06b43b3bb9edc28b77ffd8142b425", size = 194221, upload-time = "2025-09-23T20:37:19.121Z" }, -] - [[package]] name = "openapi-core" version = "0.19.5" From fc45a4cc3f42d752b80a69fc138f2b3a023aa069 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 1 Oct 2025 12:21:48 +0200 Subject: [PATCH 54/77] new parquet file example --- .github/workflows/tests.yml | 8 ++--- tests/parquet.example | Bin 0 -> 7369 bytes tests/parquet_example | Bin 5750 -> 0 bytes tests/query_tests.py | 60 ++++++++++++++++++++++++++---------- 4 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 tests/parquet.example delete mode 100644 tests/parquet_example diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a9b15e..2c64055 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: tests on: [push] env: - DUCKDB_FILENAME: tests/parquet_example + DUCKDB_FILENAME: tests/parquet.example OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} OPIK_WORKSPACE: ${{ secrets.OPIK_WORKSPACE }} @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/workflows/mcp-composite-action with: - DUCKDB_FILENAME: 'tests/parquet_example' + DUCKDB_FILENAME: 'tests/parquet.example' - name: Run tests run: uv run --directory tests/ pytest e2e_tests.py inspector: @@ -27,7 +27,7 @@ jobs: node-version: '20' - uses: ./.github/workflows/mcp-composite-action with: - DUCKDB_FILENAME: 'tests/parquet_example' + DUCKDB_FILENAME: 'tests/parquet.example' - name: Run inspector run: npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list opik: @@ -36,6 +36,6 @@ jobs: - uses: actions/checkout@v5 - uses: ./.github/workflows/mcp-composite-action with: - DUCKDB_FILENAME: 'tests/parquet_example' + DUCKDB_FILENAME: 'tests/parquet.example' - name: Run opik experiments run: uv run tests/opik/tool_calls.py diff --git a/tests/parquet.example b/tests/parquet.example new file mode 100644 index 0000000000000000000000000000000000000000..12dae89398d946159da59f62d08e88912a9784ab GIT binary patch literal 7369 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 dict[str, int]: return { - "HKQuantityTypeIdentifierBasalEnergyBurned": 18, - "HKQuantityTypeIdentifierStepCount": 10, - "HKQuantityTypeIdentifierHeartRate": 17, - "HKQuantityTypeIdentifierBodyMassIndex": 8, - "HKQuantityTypeIdentifierDietaryWater": 1, + "HKQuantityTypeIdentifierStepCount": 14, + "HKQuantityTypeIdentifierHeartRate": 14, + "HKQuantityTypeIdentifierBasalEnergyBurned": 10, + "HKQuantityTypeIdentifierDistanceWalkingRunning": 6, + "HKQuantityTypeIdentifierActiveEnergyBurned": 5, + "HKQuantityTypeIdentifierBodyMassIndex": 5, + "HKQuantityTypeIdentifierBodyMass": 5, + "HKQuantityTypeIdentifierRunningSpeed": 3, + "HKQuantityTypeIdentifierFlightsClimbed": 3, + "HKQuantityTypeIdentifierWalkingHeartRateAverage": 2, + "HKQuantityTypeIdentifierAppleExerciseTime": 2, + "HKQuantityTypeIdentifierWalkingSpeed": 2, + "HKQuantityTypeIdentifierRunningStrideLength": 2, + "HKQuantityTypeIdentifierWalkingDoubleSupportPercentage": 2, + "HKQuantityTypeIdentifierVO2Max": 2, + "HKQuantityTypeIdentifierRunningVerticalOscillation": 2, + "HKQuantityTypeIdentifierOxygenSaturation": 2, + "HKQuantityTypeIdentifierAppleWalkingSteadiness": 2, + "HKQuantityTypeIdentifierRunningGroundContactTime": 2, + "HKQuantityTypeIdentifierRestingHeartRate": 2, + "HKQuantityTypeIdentifierStairDescentSpeed": 2, + "HKQuantityTypeIdentifierHeadphoneAudioExposure": 2, + "HKQuantityTypeIdentifierHeartRateVariabilitySDNN": 2, + "HKQuantityTypeIdentifierWalkingStepLength": 2, + "HKQuantityTypeIdentifierAppleStandTime": 2, + "HKQuantityTypeIdentifierWalkingAsymmetryPercentage": 2, + "HKQuantityTypeIdentifierRunningPower": 2, + "HKQuantityTypeIdentifierEnvironmentalAudioExposure": 2, + "HKQuantityTypeIdentifierStairAscentSpeed": 2, + "HKQuantityTypeIdentifierRespiratoryRate": 2, + "HKQuantityTypeIdentifierHeight": 1 } @@ -55,6 +81,7 @@ def statistics() -> list[dict[str, Any]]: def trend_data() -> list[dict[str, Any]]: return get_trend_data_from_duckdb( record_type="HKQuantityTypeIdentifierBasalEnergyBurned", + interval="year" ) @@ -67,13 +94,13 @@ def value_search() -> list[dict[str, Any]]: def test_summary(summary: list[dict[str, Any]], counts: dict[str, int]) -> None: - assert len(summary) == 5 + assert len(summary) == len(counts) for record in summary: assert record["count"] == counts[record["type"]] def test_records(records: list[dict[str, Any]]) -> None: - assert len(records) == 3 + assert len(records) == 2 for record in records: assert 65 < record["value"] < 90 assert record["type"] == "HKQuantityTypeIdentifierStepCount" @@ -83,19 +110,20 @@ def test_statistics(statistics: list[dict[str, Any]] | dict[str, Any]) -> None: assert len(statistics) == 1 # turn list containing 1 dict into a dict stats: dict[str, Any] = statistics[0] - assert stats["min"] == 3 - assert stats["max"] == 98 - assert stats["count"] == 10 + assert stats["min"] == 13 + assert stats["max"] == 4567 + assert stats["count"] == 14 -def test_trend_data(trend_data: list[dict[str, Any]]) -> None: - assert len(trend_data) == 1 +def test_trend_data(trend_data: list[dict[str, Any]] | dict[str, Any]) -> None: + assert len(trend_data) == 3 # turn list containing 1 dict into a dict data: dict[str, Any] = trend_data[0] - assert data["min"] == data["max"] == 0.086 - assert data["count"] == 18 + assert data["count"] == 10 + assert data["min"] == 0.086 + assert data["max"] == 2.15 # floating point values not exactly matching - assert 0.999 < data["sum"] / (18 * 0.086) < 1.001 + assert 0.999 < data["sum"] / 11 < 1.001 def test_value_search(value_search: list[dict[str, Any]]) -> None: From 4a801dd1a94b0a7107506841ac3803857fa322f9 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 1 Oct 2025 12:24:06 +0200 Subject: [PATCH 55/77] changed test path --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2c64055..1d8efec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: with: DUCKDB_FILENAME: 'tests/parquet.example' - name: Run tests - run: uv run --directory tests/ pytest e2e_tests.py + run: uv run --directory tests/ pytest query_tests.py inspector: runs-on: ubuntu-latest steps: From 333a386dea82ea0b4f0240d8c32f2fd132718721 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 1 Oct 2025 12:26:28 +0200 Subject: [PATCH 56/77] fixed trend data test --- tests/query_tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/query_tests.py b/tests/query_tests.py index 7c3d508..982267e 100644 --- a/tests/query_tests.py +++ b/tests/query_tests.py @@ -116,14 +116,14 @@ def test_statistics(statistics: list[dict[str, Any]] | dict[str, Any]) -> None: def test_trend_data(trend_data: list[dict[str, Any]] | dict[str, Any]) -> None: - assert len(trend_data) == 3 + assert len(trend_data) == 2 # turn list containing 1 dict into a dict data: dict[str, Any] = trend_data[0] - assert data["count"] == 10 + assert data["count"] == 5 assert data["min"] == 0.086 - assert data["max"] == 2.15 + assert data["max"] == 0.086 # floating point values not exactly matching - assert 0.999 < data["sum"] / 11 < 1.001 + assert 0.999 < data["sum"] / (0.086 * 5) < 1.001 def test_value_search(value_search: list[dict[str, Any]]) -> None: From 723d1ccebb84434b68b6bc3a5d3d04ca45f87eab Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 1 Oct 2025 12:47:41 +0200 Subject: [PATCH 57/77] test debug for pipeline --- tests/opik/tool_calls.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index 56e5ee6..0ce87f7 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -102,6 +102,7 @@ def evaluation_task(dataset_item): task_threads=1, ) +print(tool_call_evals.test_results) output_test_evals = evaluate( experiment_name="JudgeOutputExperiment", @@ -110,3 +111,5 @@ def evaluation_task(dataset_item): scoring_metrics=[Hallucination(), LevenshteinRatio(), AnswerRelevance(require_context=False)], task_threads=1, ) + +print(output_test_evals.test_results) \ No newline at end of file From f3fa00e1ab41839c5ae6d43f2dfe5b9f586aa772 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 1 Oct 2025 13:45:33 +0200 Subject: [PATCH 58/77] print test output in pipeline --- tests/opik/tool_calls.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index 0ce87f7..14d47cd 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -102,7 +102,19 @@ def evaluation_task(dataset_item): task_threads=1, ) -print(tool_call_evals.test_results) + +for i, test in enumerate(tool_call_evals.test_results, 1): + try: + print() + print(f"------- Test {i} -------") + print("Test query: ", test.test_case.scoring_inputs['input']) + print("Expected tool call: ", test.test_case.scoring_inputs['expected_tool_calls'][0]['function_name']) + print("Called tool: ", test.test_case.scoring_inputs['tool_calls'][0]['function_name']) + print("----------------------") + print() + except Exception as e: + raise RuntimeError(f"Test {i} failed: {e}") + output_test_evals = evaluate( experiment_name="JudgeOutputExperiment", @@ -112,4 +124,16 @@ def evaluation_task(dataset_item): task_threads=1, ) -print(output_test_evals.test_results) \ No newline at end of file +# print(output_test_evals.test_results) + +for i, test in enumerate(output_test_evals.test_results, 1): + try: + print() + print(f"------- Test {i} -------") + print("Test query: ", test.test_case.scoring_inputs['input']) + print("Expected output: ", test.test_case.scoring_inputs['expected_output']) + print("Got output: ", test.test_case.task_output['output']) + print("----------------------") + print() + except Exception as e: + raise RuntimeError(f"Test {i} failed: {e}") \ No newline at end of file From 7dca53a6ea61d0f5e4b60074e87e366bdc5ffe2e Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 1 Oct 2025 14:29:22 +0200 Subject: [PATCH 59/77] readme merge conflict --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b50df4..3886eb4 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Want to try it out? **[πŸš€ Getting Started](docs/getting-started.md)** ## πŸ’Ό About Momentum This project is part of Momentum’s open-source ecosystem, where we make healthcare technology more secure, interoperable, and AI-ready. Our goal is to help HealthTech teams adopt standards such as FHIR safely and efficiently. We are healthcare AI development experts, recognized by FT1000, Deloitte Fast 50, and Forbes for building scalable, HIPAA-compliant solutions that power next-generation healthcare innovation. -πŸ“– Want to learn from our experience? Read our insights β†’ themomentum.ai/blog. +πŸ“– Want to learn from our experience? Read our insights β†’ themomentum.ai/blog. Interested? Let's talk!
From c5a5924f82be19c9a1e29671383e92dcab0161ed Mon Sep 17 00:00:00 2001 From: Jakub Czajka Date: Wed, 1 Oct 2025 16:27:55 +0200 Subject: [PATCH 60/77] Create tests.md --- docs/tests.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/tests.md diff --git a/docs/tests.md b/docs/tests.md new file mode 100644 index 0000000..e889473 --- /dev/null +++ b/docs/tests.md @@ -0,0 +1,49 @@ +(dodaj strzaΕ‚ke do powrotu) +(i referencje w readme) + +## Testing + +There are 3 types of tests in this projects, all of which are included in the pipeline: + +Every test is done on [pre-prepared mock apple health data](https://gist.github.com/czajkub/7ee7a01c35990f910f034f46dbf83b66): + + +## Unit tests: + - Testing the importing of XML data to .parquet and database calls to DuckDB + +## MCP Inspector tests: + - Uses the [inspector](https://modelcontextprotocol.io/docs/tools/inspector) provided by anthropic to test connection to the server hosted with streamable http + - Mainly used in the pipeline, but can be run locally + +## Opik tests: + - End-to-End tests using an agent created from [this](https://github.com/the-momentum/python-ai-kit) AI development kit + - Two types of tests: + - Checking whether the correct tool was called + - Judging the answer from an LLM by three metrics: + - Answer relevancy: whether the answer is relevant to the user's question + - Hallucination: whether the answer contains misleading or false information + - Levenshtein ratio: Heuristic checking the text structure similarity + +# How to run tests locally: +- ### Unit tests: +```bash +pytest tests/query_tests.py +``` + +Before running the next tests, make sure you have the server up and running: +```bash +uv run fastmcp run -t http app/main.py +``` + +- ### Inspector tests: +```bash +npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport http --method tools/list +``` + +- ### Opik tests: +(dodaj zdjΔ™cie do dodania zapytaΕ„ do eksperymentΓ³w) +Make sure your `OPIK_WORKSPACE` and `OPIK_API_KEY` environmental variables are set +(Opik workspace refers to your profile name and not project name) +```bash + uv run tests/opik/tool_calls.py +``` From f8afe7e85f24f44d2673f3ac032e172d2d0b9450 Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 2 Oct 2025 10:44:08 +0200 Subject: [PATCH 61/77] rollback unstable changes --- app/mcp/v1/mcp.py | 8 ++-- app/services/duckdb_client.py | 2 - scripts/duckdb_importer.py | 83 ++++++++------------------------- scripts/xml_exporter.py | 87 ++++++++--------------------------- 4 files changed, 40 insertions(+), 140 deletions(-) diff --git a/app/mcp/v1/mcp.py b/app/mcp/v1/mcp.py index e7d99fc..2a5d759 100644 --- a/app/mcp/v1/mcp.py +++ b/app/mcp/v1/mcp.py @@ -1,10 +1,10 @@ from fastmcp import FastMCP -from app.mcp.v1.tools import duckdb_reader +from app.mcp.v1.tools import duckdb_reader, ch_reader, es_reader, xml_reader mcp_router = FastMCP(name="Main MCP") mcp_router.mount(duckdb_reader.duckdb_reader_router) -# mcp_router.mount(ch_reader.ch_reader_router) -# mcp_router.mount(es_reader.es_reader_router) -# mcp_router.mount(xml_reader.xml_reader_router) +mcp_router.mount(ch_reader.ch_reader_router) +mcp_router.mount(es_reader.es_reader_router) +mcp_router.mount(xml_reader.xml_reader_router) diff --git a/app/services/duckdb_client.py b/app/services/duckdb_client.py index 5ebb4be..e4d7996 100644 --- a/app/services/duckdb_client.py +++ b/app/services/duckdb_client.py @@ -24,8 +24,6 @@ def __post_init__(self): else: self.path = Path(self.path) - if isinstance(self.path, Path) and not self.path.exists(): - raise FileNotFoundError(f"Parquet file not found: {self.path}") @staticmethod def format_response(response: DuckDBPyRelation) -> list[dict[str, Any]]: diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index 80ec8a8..bd45c61 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -12,82 +12,35 @@ def __init__(self): XMLExporter.__init__(self) DuckDBClient.__init__(self) - chunk_files = [] - - def write_to_file(self, index: int, df: pl.DataFrame) -> None: - try: - if "workoutActivityType" in df.columns: - chunk_file: Path = Path(f"workouts.chunk_{index}.parquet") - print(f"processed {index * self.chunk_size} docs") - df.write_parquet(chunk_file, compression="zstd", compression_level=1) - self.chunk_files.append(chunk_file) - elif "type" in df.columns: - chunk_file: Path = Path(f"records.chunk_{index}.parquet") - print(f"processed {index * self.chunk_size} docs") - df.write_parquet(chunk_file, compression="zstd", compression_level=1) - self.chunk_files.append(chunk_file) - else: - for chunk_file in self.chunk_files: - os.remove(chunk_file) - raise RuntimeError("Missing required fields in export file") - except Exception: - for file in self.chunk_files: - os.remove(file) - raise RuntimeError(f"Failed to write chunk file to disk: {chunk_file}") - def export_xml(self) -> None: + chunk_files = [] for i, docs in enumerate(self.parse_xml(), 1): df: pl.DataFrame = pl.DataFrame(docs) - self.write_to_file(i, df) + chunk_file: Path = Path(f"data.chunk_{i}.parquet") + print(f"processed {i * self.chunk_size} docs") + df.write_parquet(chunk_file, compression="zstd", compression_level=1) + chunk_files.append(chunk_file) + print(f"written {i * self.chunk_size} docs") - record_chunk_dfs: list[pl.DataFrame] = [] - workout_chunk_dfs: list[pl.DataFrame] = [] - stat_chunk_dfs: list[pl.DataFrame] = [] - # reference_columns: list[str] = [] + chunk_dfs: list[pl.DataFrame] = [] + reference_columns: list[str] = [] - for chunk_file in self.chunk_files: + for chunk_file in chunk_files: df = pl.read_parquet(chunk_file) - if "value" in df.columns: - df = df.select(self.RECORD_COLUMNS) - record_chunk_dfs.append(df) - elif "device" in df.columns: - df = df.select(self.WORKOUT_COLUMNS) - workout_chunk_dfs.append(df) - elif "sum" in df.columns: - df = df.select(self.WORKOUT_STATS_COLUMNS) - stat_chunk_dfs.append(df) - record_df = None - workout_df = None - stat_df = None + if not reference_columns: + reference_columns = df.columns + + df = df.select(reference_columns) + chunk_dfs.append(df) - try: - if record_chunk_dfs: - record_df = pl.concat(record_chunk_dfs) - if workout_chunk_dfs: - workout_df = pl.concat(workout_chunk_dfs) - if stat_chunk_dfs: - stat_df = pl.concat(stat_chunk_dfs) - except Exception as e: - for f in self.chunk_files: - os.remove(f) - raise RuntimeError(f"Failed to concatenate dataframes: {str(e)}") - try: - if record_df is not None: - record_df.write_parquet(f"{self.path / 'records.parquet'}", compression="zstd") - if workout_df is not None: - workout_df.write_parquet(f"{self.path / 'workouts.parquet'}", compression="zstd") - if stat_df is not None: - stat_df.write_parquet(f"{self.path / 'stats.parquet'}", compression="zstd") - except Exception as e: - for f in self.chunk_files: - os.remove(f) - raise RuntimeError(f"Failed to write to path {self.path}: {str(e)}") + combined_df = pl.concat(chunk_dfs) + combined_df.write_parquet(f"{self.path}", compression="zstd") - for f in self.chunk_files: + for f in chunk_files: os.remove(f) if __name__ == "__main__": importer = ParquetImporter() - importer.export_xml() + importer.export_xml() \ No newline at end of file diff --git a/scripts/xml_exporter.py b/scripts/xml_exporter.py index b9a89de..501d270 100644 --- a/scripts/xml_exporter.py +++ b/scripts/xml_exporter.py @@ -3,7 +3,6 @@ from typing import Any, Generator from xml.etree import ElementTree as ET -import polars as pl from pandas import DataFrame from app.config import settings @@ -16,12 +15,12 @@ def __init__(self): DATE_FIELDS: tuple[str, ...] = ("startDate", "endDate", "creationDate") DEFAULT_VALUES: dict[str, str] = { - "unit": "", - "sourceVersion": "", - "device": "", - "value": "", + "unit": "unknown", + "sourceVersion": "unknown", + "device": "unknown", + "value": "unknown", } - RECORD_COLUMNS: tuple[str, ...] = ( + COLUMN_NAMES: tuple[str, ...] = ( "type", "sourceVersion", "sourceName", @@ -33,24 +32,8 @@ def __init__(self): "value", "textValue", ) - WORKOUT_COLUMNS: tuple[str, ...] = ( - "type", - "duration", - "durationUnit", - "sourceName", - "startDate", - "endDate", - "creationDate", - ) - WORKOUT_STATS_COLUMNS: tuple[str, ...] = ( - "type", - "startDate", - "endDate", - "sum", - "unit", - ) - def update_record(self, table: str, document: dict[str, Any]) -> dict[str, Any]: + def update_record(self, document: dict[str, Any]) -> dict[str, Any]: """ Updates records to fill out columns without specified data: There are 9 columns that need to be filled out, and there are 4 columns @@ -58,72 +41,38 @@ def update_record(self, table: str, document: dict[str, Any]) -> dict[str, Any]: Additionally a textValue field is added for querying text values """ for field in self.DATE_FIELDS: - if field in document: - document[field] = datetime.strptime(document[field], "%Y-%m-%d %H:%M:%S %z") - - if table == "record": - if len(document) != 9: - document.update({k: v for k, v in self.DEFAULT_VALUES.items() if k not in document}) + document[field] = datetime.strptime(document[field], "%Y-%m-%d %H:%M:%S %z") - document["textValue"] = document["value"] + if len(document) != 9: + document.update({k: v for k, v in self.DEFAULT_VALUES.items() if k not in document}) - try: - document["value"] = float(document["value"]) - except (TypeError, ValueError): - document["value"] = 0.0 + document["textValue"] = document["value"] - elif table == "workout": - document["type"] = document.pop("workoutActivityType") - - try: - document["duration"] = float(document["duration"]) - except (TypeError, ValueError): - document["duration"] = 0.0 + try: + document["value"] = float(document["value"]) + except (TypeError, ValueError): + document["value"] = 0.0 return document - def parse_xml(self) -> Generator[pl.DataFrame, Any, None]: + def parse_xml(self) -> Generator[DataFrame, Any, None]: """ Parses the XML file and yields pandas dataframes of specified chunk_size. Extracts attributes from each Record element. """ records: list[dict[str, Any]] = [] - workouts: list[dict[str, Any]] = [] - workout_stats: list[dict[str, Any]] = [] for event, elem in ET.iterparse(self.xml_path, events=("start",)): if elem.tag == "Record" and event == "start": if len(records) >= self.chunk_size: - # yield pl.DataFrame(records) - yield DataFrame(records).reindex(columns=self.RECORD_COLUMNS) + yield DataFrame(records).reindex(columns=self.COLUMN_NAMES) records = [] record: dict[str, Any] = elem.attrib.copy() # fill out empty cells if they exist and convert dates to datetime - self.update_record("record", record) + self.update_record(record) records.append(record) - - elif elem.tag == "Workout" and event == "start": - if len(workouts) >= self.chunk_size: - yield DataFrame(workouts).reindex(columns=self.WORKOUT_COLUMNS) - workouts = [] - workout: dict[str, Any] = elem.attrib.copy() - - for stat in elem: - if stat.tag != "WorkoutStatistics": - continue - statistic = stat.attrib.copy() - self.update_record("stat", statistic) - workout_stats.append(statistic) - if len(workout_stats) >= self.chunk_size: - yield DataFrame(workout_stats).reindex(columns=self.WORKOUT_STATS_COLUMNS) - workout_stats = [] - - self.update_record("workout", workout) - workouts.append(workout) elem.clear() # yield remaining records - yield DataFrame(records).reindex(columns=self.RECORD_COLUMNS) - yield DataFrame(workouts).reindex(columns=self.WORKOUT_COLUMNS) - yield DataFrame(workout_stats).reindex(columns=self.WORKOUT_STATS_COLUMNS) + yield DataFrame(records).reindex(columns=self.COLUMN_NAMES) \ No newline at end of file From 674d75df86eddf76390ced61ce032e09f224f44f Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 2 Oct 2025 10:45:52 +0200 Subject: [PATCH 62/77] remove redundant tests --- tests/e2e_tests.py | 79 ---------------------------------------------- 1 file changed, 79 deletions(-) delete mode 100644 tests/e2e_tests.py diff --git a/tests/e2e_tests.py b/tests/e2e_tests.py deleted file mode 100644 index 1d786bb..0000000 --- a/tests/e2e_tests.py +++ /dev/null @@ -1,79 +0,0 @@ -import asyncio -import pytest - -from pydantic_ai import capture_run_messages -from pydantic_ai.messages import ToolCallPart - -from tests.agent import AgentManager - - -agent_manager = AgentManager() -asyncio.run(agent_manager.initialize()) - - -async def tool_call_template(query: str, tool_name: str): - with capture_run_messages() as messages: - try: - await agent_manager.handle_message(query) - except ExceptionGroup: - pytest.fail("Failed to connect with MCP server", False) - finally: - print(messages) - assert len(messages) == 4 - resp = messages[1] - assert isinstance(resp.parts[0], ToolCallPart) - assert resp.parts[0].tool_name == tool_name - - -async def llm_opinion_template(query: str, expected: str): - with capture_run_messages() as messages: - try: - await agent_manager.handle_message(query) - assert len(messages) == 4 - output = messages[3] - resp = await agent_manager.handle_message(f""" - You are a judge that determines on a scale from 0-100% - how close two messages are to each other. You will receive - two inputs. Respond only with a percentage, e.g. "81", without % sign. - Consider things like missing or differing data, you can ignore - things like honorifics, your highest priority is data itself - - Input nr. 1: - {output.parts[0].content} - - Input nr. 2: - {expected} - - """) - percent = int(resp) - assert 75 <= percent <= 100 - - except ExceptionGroup: - pytest.fail("Failed to connect with MCP server", False) - finally: - print(messages) - - - -@pytest.mark.asyncio -async def test_summary(): - await tool_call_template("please give me a summary of my health from duckdb", - 'get_health_summary_duckdb') - -@pytest.mark.asyncio -async def test_judge(): - await llm_opinion_template("please give me a summary of my health from duckdb", - """Here's a summary of your health data from DuckDB: - - **Heart Rate**: 17 records - **Basal Energy Burned**: 17 records - - **Step Count**: 10 records - **Body Mass Index (BMI)**: 8 records - - **Dietary Water Intake**: 1 record""") - - - -# @pytest.mark.asyncio -# async def test_stats(): -# await query_template("please give me statistics of my step counts from duckdb") -# -# @pytest.mark.asyncio -# async def test_trend(): -# await query_template("please give me trend data for my heart rate") From 920a617d42647fee751d178b72882eee659372d8 Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 2 Oct 2025 10:47:43 +0200 Subject: [PATCH 63/77] lint --- app/mcp/v1/mcp.py | 2 +- app/services/duckdb_client.py | 1 - scripts/duckdb_importer.py | 2 +- scripts/xml_exporter.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/mcp/v1/mcp.py b/app/mcp/v1/mcp.py index 2a5d759..c698502 100644 --- a/app/mcp/v1/mcp.py +++ b/app/mcp/v1/mcp.py @@ -1,6 +1,6 @@ from fastmcp import FastMCP -from app.mcp.v1.tools import duckdb_reader, ch_reader, es_reader, xml_reader +from app.mcp.v1.tools import ch_reader, duckdb_reader, es_reader, xml_reader mcp_router = FastMCP(name="Main MCP") diff --git a/app/services/duckdb_client.py b/app/services/duckdb_client.py index e4d7996..b9b4a94 100644 --- a/app/services/duckdb_client.py +++ b/app/services/duckdb_client.py @@ -24,7 +24,6 @@ def __post_init__(self): else: self.path = Path(self.path) - @staticmethod def format_response(response: DuckDBPyRelation) -> list[dict[str, Any]]: return response.df().to_dict(orient="records") diff --git a/scripts/duckdb_importer.py b/scripts/duckdb_importer.py index bd45c61..71bc1c4 100644 --- a/scripts/duckdb_importer.py +++ b/scripts/duckdb_importer.py @@ -43,4 +43,4 @@ def export_xml(self) -> None: if __name__ == "__main__": importer = ParquetImporter() - importer.export_xml() \ No newline at end of file + importer.export_xml() diff --git a/scripts/xml_exporter.py b/scripts/xml_exporter.py index 501d270..2dedde3 100644 --- a/scripts/xml_exporter.py +++ b/scripts/xml_exporter.py @@ -75,4 +75,4 @@ def parse_xml(self) -> Generator[DataFrame, Any, None]: elem.clear() # yield remaining records - yield DataFrame(records).reindex(columns=self.COLUMN_NAMES) \ No newline at end of file + yield DataFrame(records).reindex(columns=self.COLUMN_NAMES) From 2f3879352ca58a45a326bd4e5b0c599a7311a8fa Mon Sep 17 00:00:00 2001 From: Jakub Czajka Date: Thu, 2 Oct 2025 10:58:00 +0200 Subject: [PATCH 64/77] Update tests.md --- docs/tests.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/tests.md b/docs/tests.md index e889473..8accb08 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -1,5 +1,4 @@ -(dodaj strzaΕ‚ke do powrotu) -(i referencje w readme) +[← Back to README](../README.md) ## Testing @@ -47,3 +46,22 @@ Make sure your `OPIK_WORKSPACE` and `OPIK_API_KEY` environmental variables are s ```bash uv run tests/opik/tool_calls.py ``` + +To add new tests, you can either do it in the code ([example from opik](https://www.comet.com/docs/opik/evaluation/manage_datasets)): +```python +import opik +# Get or create a dataset +client = opik.Opik() +dataset = client.get_or_create_dataset(name="My dataset") +# Add dataset items to it +dataset.insert([ + {"user_question": "Hello, world!", "expected_output": {"assistant_answer": "Hello, world!"}}, + {"user_question": "What is the capital of France?", "expected_output": {"assistant_answer": "Paris"}}, +]) +``` + +[← Back to README](../README.md) +Or add it on the website: +image + +When adding tool call questions, make sure the `input` and `tool_call` values are present, and when adding output checks make sure `input` and `expected_output` are set correctly. From a83535d0507463a5daf4fa475dbbe63415889ad3 Mon Sep 17 00:00:00 2001 From: Jakub Czajka Date: Thu, 2 Oct 2025 11:02:59 +0200 Subject: [PATCH 65/77] Update tests.md --- docs/tests.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/tests.md b/docs/tests.md index 8accb08..f60b8fd 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -1,30 +1,30 @@ [← Back to README](../README.md) -## Testing +## Testing πŸ§ͺ There are 3 types of tests in this projects, all of which are included in the pipeline: Every test is done on [pre-prepared mock apple health data](https://gist.github.com/czajkub/7ee7a01c35990f910f034f46dbf83b66): -## Unit tests: +## Unit tests πŸ”§: - Testing the importing of XML data to .parquet and database calls to DuckDB -## MCP Inspector tests: +## MCP Inspector tests πŸ”: - Uses the [inspector](https://modelcontextprotocol.io/docs/tools/inspector) provided by anthropic to test connection to the server hosted with streamable http - Mainly used in the pipeline, but can be run locally -## Opik tests: +## Opik tests πŸ€–: - End-to-End tests using an agent created from [this](https://github.com/the-momentum/python-ai-kit) AI development kit - Two types of tests: - Checking whether the correct tool was called - Judging the answer from an LLM by three metrics: - - Answer relevancy: whether the answer is relevant to the user's question - - Hallucination: whether the answer contains misleading or false information - - Levenshtein ratio: Heuristic checking the text structure similarity + - Answer relevancy: whether the answer is relevant to the user's question 🎯 + - Hallucination: whether the answer contains misleading or false information 🚫 + - Levenshtein ratio: Heuristic checking the text structure similarity πŸ“Š -# How to run tests locally: -- ### Unit tests: +# How to run tests locally πŸ’»: +- ### Unit tests πŸ”§: ```bash pytest tests/query_tests.py ``` @@ -34,19 +34,23 @@ Before running the next tests, make sure you have the server up and running: uv run fastmcp run -t http app/main.py ``` -- ### Inspector tests: +- ### Inspector tests πŸ”: ```bash npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport http --method tools/list ``` -- ### Opik tests: -(dodaj zdjΔ™cie do dodania zapytaΕ„ do eksperymentΓ³w) +- ### Opik tests πŸ€–: Make sure your `OPIK_WORKSPACE` and `OPIK_API_KEY` environmental variables are set (Opik workspace refers to your profile name and not project name) ```bash uv run tests/opik/tool_calls.py ``` +### How to run Opik tests in pipeline: +- Create an account on Opik if you already haven't +- Copy your `OPIK_API_KEY` and `OPIK_WORKSPACE` to Github secrets + + To add new tests, you can either do it in the code ([example from opik](https://www.comet.com/docs/opik/evaluation/manage_datasets)): ```python import opik @@ -60,8 +64,9 @@ dataset.insert([ ]) ``` -[← Back to README](../README.md) Or add it on the website: image When adding tool call questions, make sure the `input` and `tool_call` values are present, and when adding output checks make sure `input` and `expected_output` are set correctly. + +[← Back to README](../README.md) From 440201492bca9d9ddb9910a209cdaf41eb593aef Mon Sep 17 00:00:00 2001 From: czajkub Date: Thu, 2 Oct 2025 11:35:34 +0200 Subject: [PATCH 66/77] test improvement --- tests/opik/tool_calls.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index 14d47cd..327a71d 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -1,6 +1,7 @@ import asyncio import os +from pydantic_ai.messages import ModelRequest, ModelResponse, ToolCallPart from opik import Opik from opik.evaluation import evaluate from opik.evaluation.metrics import ( @@ -25,8 +26,8 @@ def __init__(self, name: str = "tool_selection_quality"): def score(self, tool_calls, expected_tool_calls, **kwargs): try: - actual_tool = tool_calls[0]["function_name"] - expected_tool = expected_tool_calls[0]["function_name"] + actual_tool = tool_calls["function_name"] + expected_tool = expected_tool_calls["function_name"] if actual_tool == expected_tool: return score_result.ScoreResult( @@ -48,6 +49,18 @@ def score(self, tool_calls, expected_tool_calls, **kwargs): ) +def extract_tool_call(messages: list[ModelRequest | ModelResponse]) -> str | None: + for message in messages: + if isinstance(message, ModelRequest): + continue + part = message.parts[0] + try: + return part.tool_name + except Exception: + continue + return None + + def evaluation_task(dataset_item): try: user_message_content = dataset_item["input"] @@ -56,23 +69,23 @@ def evaluation_task(dataset_item): resp = agent_manager.agent.run_sync(user_message_content) result = resp.new_messages() - tool_calls = [{"function_name": result[1].parts[0].tool_name, - "function_parameters": {}}] + tool_calls = {"function_name": extract_tool_call(result), + "function_parameters": {}} return { "input": user_message_content, "output": resp.output, "reference": reference, "tool_calls": tool_calls, - "expected_tool_calls": [{"function_name": expected_tool, "function_parameters": {}}] + "expected_tool_calls": {"function_name": expected_tool, "function_parameters": {}} } except Exception as e: return { "input": dataset_item.get("input", {}), "output": "Error processing input.", "reference": dataset_item.get("expected_output", ""), - "tool_calls": [], - "expected_tool_calls": [{"function_name": "unknown", "function_parameters": {}}], + "tool_calls": {}, + "expected_tool_calls": {"function_name": "unknown", "function_parameters": {}}, "error": str(e) } @@ -108,12 +121,12 @@ def evaluation_task(dataset_item): print() print(f"------- Test {i} -------") print("Test query: ", test.test_case.scoring_inputs['input']) - print("Expected tool call: ", test.test_case.scoring_inputs['expected_tool_calls'][0]['function_name']) - print("Called tool: ", test.test_case.scoring_inputs['tool_calls'][0]['function_name']) + print("Expected tool call: ", test.test_case.scoring_inputs['expected_tool_calls']['function_name']) + print("Called tool: ", test.test_case.scoring_inputs['tool_calls']['function_name']) print("----------------------") print() except Exception as e: - raise RuntimeError(f"Test {i} failed: {e}") + print(f"Test {i} failed: {e}") output_test_evals = evaluate( @@ -124,7 +137,6 @@ def evaluation_task(dataset_item): task_threads=1, ) -# print(output_test_evals.test_results) for i, test in enumerate(output_test_evals.test_results, 1): try: @@ -136,4 +148,4 @@ def evaluation_task(dataset_item): print("----------------------") print() except Exception as e: - raise RuntimeError(f"Test {i} failed: {e}") \ No newline at end of file + print(f"Test {i} failed: {e}") \ No newline at end of file From a8de0be595388cfed28408747778caa0e5510d26 Mon Sep 17 00:00:00 2001 From: czajkub Date: Tue, 7 Oct 2025 21:35:20 +0200 Subject: [PATCH 67/77] add config for opik in .env --- app/config.py | 6 ++++++ config/.env.example | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/config.py b/app/config.py index a2fe6c3..d407ebb 100644 --- a/app/config.py +++ b/app/config.py @@ -37,6 +37,12 @@ class Settings(BaseSettings): RAW_XML_PATH: str = "raw.xml" XML_SAMPLE_SIZE: int = 1000 + # Opik tests + OPENAI_API_KEY: str = "" + OPIK_WORKSPACE: str = "" + OPIK_API_KEY: str = "" + + @field_validator("BACKEND_CORS_ORIGINS", mode="after") @classmethod def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str: diff --git a/config/.env.example b/config/.env.example index 035f920..b7e203b 100644 --- a/config/.env.example +++ b/config/.env.example @@ -1,9 +1,9 @@ ES_USER="elastic" ES_PASSWORD="elastic" ES_HOST="localhost" -CH_DIRNAME="applehealth.chdb" -CH_DB_NAME="applehealth" -CH_TABLE_NAME="data" DUCKDB_FILENAME="applehealth.parquet" CHUNK_SIZE="50000" RAW_XML_PATH="raw.xml" +OPENAI_API_KEY="sk-proj-***" +OPIK_WORKSPACE="username" +OPIK_API_KEY="abcdef12345" \ No newline at end of file From 47452da7a0da0ae19544f698615c737a7a64764e Mon Sep 17 00:00:00 2001 From: Jakub Czajka Date: Tue, 7 Oct 2025 21:37:33 +0200 Subject: [PATCH 68/77] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3886eb4..c4d78a4 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Want to try it out? **[πŸš€ Getting Started](docs/getting-started.md)** - **[πŸ”§ Configuration](docs/configuration.md)** - Environment variables and settings - **[πŸ› οΈ MCP Tools](docs/mcp-tools.md)** - All available tools - **[πŸ—ΊοΈ Roadmap](docs/roadmap.md)** - Upcoming features and roadmap +- **[πŸ§ͺ Testing](docs/tests.md)** - Instructions on tests and how to run them **Need help?** Looking for guidance on use cases or implementation? Don't hesitate to ask your question in our [GitHub discussion forum](https://github.com/the-momentum/apple-health-mcp-server/discussions)! You'll also find interesting use cases, tips, and community insights there. From b0674e79bce182f721af87d3bcd88c4a4781376c Mon Sep 17 00:00:00 2001 From: Jakub Czajka Date: Tue, 7 Oct 2025 21:39:26 +0200 Subject: [PATCH 69/77] Update tests.md --- docs/tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tests.md b/docs/tests.md index f60b8fd..d934509 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -43,7 +43,7 @@ npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport Make sure your `OPIK_WORKSPACE` and `OPIK_API_KEY` environmental variables are set (Opik workspace refers to your profile name and not project name) ```bash - uv run tests/opik/tool_calls.py +uv run tests/opik/tool_calls.py ``` ### How to run Opik tests in pipeline: From 2b8185ebbf1d5c75317c72413d30d961e8a0bb15 Mon Sep 17 00:00:00 2001 From: czajkub Date: Tue, 7 Oct 2025 21:42:13 +0200 Subject: [PATCH 70/77] lint --- app/config.py | 1 - config/.env.example | 2 +- docs/getting-started.md | 4 ++-- docs/tests.md | 10 +++++----- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/config.py b/app/config.py index d407ebb..66dc115 100644 --- a/app/config.py +++ b/app/config.py @@ -42,7 +42,6 @@ class Settings(BaseSettings): OPIK_WORKSPACE: str = "" OPIK_API_KEY: str = "" - @field_validator("BACKEND_CORS_ORIGINS", mode="after") @classmethod def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str: diff --git a/config/.env.example b/config/.env.example index b7e203b..cc1aa78 100644 --- a/config/.env.example +++ b/config/.env.example @@ -6,4 +6,4 @@ CHUNK_SIZE="50000" RAW_XML_PATH="raw.xml" OPENAI_API_KEY="sk-proj-***" OPIK_WORKSPACE="username" -OPIK_API_KEY="abcdef12345" \ No newline at end of file +OPIK_API_KEY="abcdef12345" diff --git a/docs/getting-started.md b/docs/getting-started.md index 628912b..069cd23 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -41,8 +41,8 @@ Follow these steps to set up Apple Health MCP Server in your environment. - Run `make duckdb` to create a parquet file with your exported XML data - If you want to connect to the file through http(s): - The only thing you need to do is change the .env path, e.g. `localhost:8080/applehealth.parquet` - - If you want an example on how to host the files locally, run `uv run tests/fileserver.py` - + - If you want an example on how to host the files locally, run `uv run tests/fileserver.py` + ## Configuration Files diff --git a/docs/tests.md b/docs/tests.md index f60b8fd..a9d193b 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -9,11 +9,11 @@ Every test is done on [pre-prepared mock apple health data](https://gist.github. ## Unit tests πŸ”§: - Testing the importing of XML data to .parquet and database calls to DuckDB - + ## MCP Inspector tests πŸ”: - Uses the [inspector](https://modelcontextprotocol.io/docs/tools/inspector) provided by anthropic to test connection to the server hosted with streamable http - Mainly used in the pipeline, but can be run locally - + ## Opik tests πŸ€–: - End-to-End tests using an agent created from [this](https://github.com/the-momentum/python-ai-kit) AI development kit - Two types of tests: @@ -22,13 +22,13 @@ Every test is done on [pre-prepared mock apple health data](https://gist.github. - Answer relevancy: whether the answer is relevant to the user's question 🎯 - Hallucination: whether the answer contains misleading or false information 🚫 - Levenshtein ratio: Heuristic checking the text structure similarity πŸ“Š - + # How to run tests locally πŸ’»: -- ### Unit tests πŸ”§: +- ### Unit tests πŸ”§: ```bash pytest tests/query_tests.py ``` - + Before running the next tests, make sure you have the server up and running: ```bash uv run fastmcp run -t http app/main.py From 06e5f92af05deb60e620648126c835aeb5c2b353 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 8 Oct 2025 14:48:33 +0200 Subject: [PATCH 71/77] add test for workouts and tweak current tests --- app/services/health/duckdb_queries.py | 4 ++-- tests/query_tests.py | 31 +++++++++++++++++++++++++-- uv.lock | 2 ++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index 8f3621e..d1973f1 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -141,8 +141,8 @@ def main() -> None: pars = HealthRecordSearchParams( limit=20, record_type="HKWorkoutActivityTypeRunning", - date_from="2016-01-01T00:00:00+00:00", - date_to="2016-12-31T23:59:59+00:00", + min_workout_duration="45", + max_workout_duration="53" ) logger.info( f"records for search_health_records_from_duckdb: {search_health_records_from_duckdb(pars)}", diff --git a/tests/query_tests.py b/tests/query_tests.py index 982267e..2da0cc2 100644 --- a/tests/query_tests.py +++ b/tests/query_tests.py @@ -4,7 +4,7 @@ import pytest -path = Path(__file__).parent / "parquet.example" +path = Path(__file__).parent.parent / "data.duckdb" os.environ["DUCKDB_FILENAME"] = str(path) from app.schemas.record import HealthRecordSearchParams @@ -50,7 +50,15 @@ def counts() -> dict[str, int]: "HKQuantityTypeIdentifierEnvironmentalAudioExposure": 2, "HKQuantityTypeIdentifierStairAscentSpeed": 2, "HKQuantityTypeIdentifierRespiratoryRate": 2, - "HKQuantityTypeIdentifierHeight": 1 + "HKQuantityTypeIdentifierHeight": 1, + "HKWorkoutActivityTypeRunning": 24, + "HKWorkoutActivityTypeWalking": 5, + "HKWorkoutActivityTypeHiking": 3, + "HKWorkoutActivityTypeCycling": 3, + "HKWorkoutActivityTypeMixedMetabolicCardioTraining": 2, + "HKWorkoutActivityTypeHockey": 2, + "HKWorkoutActivityTypeHighIntensityIntervalTraining": 2, + "HKWorkoutActivityTypeTraditionalStrengthTraining": 2 } @@ -69,6 +77,17 @@ def records() -> list[dict[str, Any]]: ), ) +@pytest.fixture +def workouts() -> list[dict[str, Any]]: + return search_health_records_from_duckdb( + HealthRecordSearchParams( + limit=20, + record_type="HKWorkoutActivityTypeRunning", + min_workout_duration="45", + max_workout_duration="53" + ) + ) + @pytest.fixture def statistics() -> list[dict[str, Any]]: @@ -105,6 +124,14 @@ def test_records(records: list[dict[str, Any]]) -> None: assert 65 < record["value"] < 90 assert record["type"] == "HKQuantityTypeIdentifierStepCount" +def test_workouts(workouts: list[dict[str, Any]]) -> None: + # 3 records, however there are 20 total stats associated with them + assert len(workouts) == 20 + for record in workouts: + assert 30 < record["duration"] < 60 + assert record["type"] == "HKWorkoutActivityTypeRunning" + + def test_statistics(statistics: list[dict[str, Any]] | dict[str, Any]) -> None: assert len(statistics) == 1 diff --git a/uv.lock b/uv.lock index 43ebedc..93ee96c 100644 --- a/uv.lock +++ b/uv.lock @@ -172,6 +172,8 @@ code-quality = [ ] dev = [ { name = "fastapi", specifier = ">=0.116.2" }, + { name = "opik", specifier = ">=1.8.56" }, + { name = "pydantic-ai", specifier = ">=1.0.10" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=6.2.1" }, From c1b72a84f1dd744987d9d6a8627762433fcb2f9b Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 8 Oct 2025 14:57:37 +0200 Subject: [PATCH 72/77] changed example file --- .github/workflows/tests.yml | 2 +- tests/duckdb.example | Bin 0 -> 1060864 bytes tests/parquet.example | Bin 7369 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/duckdb.example delete mode 100644 tests/parquet.example diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1d8efec..1328fec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: tests on: [push] env: - DUCKDB_FILENAME: tests/parquet.example + DUCKDB_FILENAME: tests/duckdb.example OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }} OPIK_WORKSPACE: ${{ secrets.OPIK_WORKSPACE }} diff --git a/tests/duckdb.example b/tests/duckdb.example new file mode 100644 index 0000000000000000000000000000000000000000..e5b1fcb3baeb17dbfbd7bc0f32a8825c0be96465 GIT binary patch literal 1060864 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 z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk 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 z Date: Wed, 8 Oct 2025 14:58:06 +0200 Subject: [PATCH 73/77] changed workflow example --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1328fec..0b83956 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/workflows/mcp-composite-action with: - DUCKDB_FILENAME: 'tests/parquet.example' + DUCKDB_FILENAME: 'tests/duckdb.example' - name: Run tests run: uv run --directory tests/ pytest query_tests.py inspector: @@ -27,7 +27,7 @@ jobs: node-version: '20' - uses: ./.github/workflows/mcp-composite-action with: - DUCKDB_FILENAME: 'tests/parquet.example' + DUCKDB_FILENAME: 'tests/duckdb.example' - name: Run inspector run: npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list opik: @@ -36,6 +36,6 @@ jobs: - uses: actions/checkout@v5 - uses: ./.github/workflows/mcp-composite-action with: - DUCKDB_FILENAME: 'tests/parquet.example' + DUCKDB_FILENAME: 'tests/duckdb.example' - name: Run opik experiments run: uv run tests/opik/tool_calls.py From 08b3c4ed695714ef15099017fdd864c0c7a54c5e Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 8 Oct 2025 14:59:57 +0200 Subject: [PATCH 74/77] changed pytest path --- tests/opik/tool_calls.py | 2 +- tests/query_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index 327a71d..7716320 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -1,7 +1,7 @@ import asyncio import os -from pydantic_ai.messages import ModelRequest, ModelResponse, ToolCallPart +from pydantic_ai.messages import ModelRequest, ModelResponse from opik import Opik from opik.evaluation import evaluate from opik.evaluation.metrics import ( diff --git a/tests/query_tests.py b/tests/query_tests.py index 2da0cc2..c185ab5 100644 --- a/tests/query_tests.py +++ b/tests/query_tests.py @@ -4,7 +4,7 @@ import pytest -path = Path(__file__).parent.parent / "data.duckdb" +path = Path(__file__).parent / "duckdb.example" os.environ["DUCKDB_FILENAME"] = str(path) from app.schemas.record import HealthRecordSearchParams From afe214d0f89c7dac408121c31b5783e204093b2c Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 8 Oct 2025 15:02:24 +0200 Subject: [PATCH 75/77] linter --- app/services/health/duckdb_queries.py | 2 +- pyproject.toml | 2 +- tests/opik/tool_calls.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/health/duckdb_queries.py b/app/services/health/duckdb_queries.py index d1973f1..286363f 100644 --- a/app/services/health/duckdb_queries.py +++ b/app/services/health/duckdb_queries.py @@ -142,7 +142,7 @@ def main() -> None: limit=20, record_type="HKWorkoutActivityTypeRunning", min_workout_duration="45", - max_workout_duration="53" + max_workout_duration="53", ) logger.info( f"records for search_health_records_from_duckdb: {search_health_records_from_duckdb(pars)}", diff --git a/pyproject.toml b/pyproject.toml index d3e62af..02b1bf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ exclude = ["./tests/", "./docs/", "./README.md"] [tool.ruff] line-length = 100 target-version = "py313" -extend-exclude = ["tests/", "./docs/", "./README.md"] +extend-exclude = ["./tests/", "./docs/", "./README.md"] [tool.ruff.lint] select = [ diff --git a/tests/opik/tool_calls.py b/tests/opik/tool_calls.py index 7716320..3ab661e 100644 --- a/tests/opik/tool_calls.py +++ b/tests/opik/tool_calls.py @@ -148,4 +148,4 @@ def evaluation_task(dataset_item): print("----------------------") print() except Exception as e: - print(f"Test {i} failed: {e}") \ No newline at end of file + print(f"Test {i} failed: {e}") From 39e9eee66b86e23ad16d1f5170e19226d1143612 Mon Sep 17 00:00:00 2001 From: czajkub Date: Wed, 8 Oct 2025 15:16:12 +0200 Subject: [PATCH 76/77] change composite action default value --- .github/workflows/mcp-composite-action/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mcp-composite-action/action.yml b/.github/workflows/mcp-composite-action/action.yml index 2d7a641..7e8627e 100644 --- a/.github/workflows/mcp-composite-action/action.yml +++ b/.github/workflows/mcp-composite-action/action.yml @@ -6,7 +6,7 @@ inputs: DUCKDB_FILENAME: description: 'path to duckdb file' required: false - default: 'tests/parquet_example' + default: 'tests/duckdb.example' runs: using: "composite" From 2cacf9f66490d350bb6ca456c5a431a0294b58a2 Mon Sep 17 00:00:00 2001 From: Jakub Czajka Date: Wed, 8 Oct 2025 15:18:49 +0200 Subject: [PATCH 77/77] Update tests.md --- docs/tests.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tests.md b/docs/tests.md index 056b48e..323f689 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -8,10 +8,10 @@ Every test is done on [pre-prepared mock apple health data](https://gist.github. ## Unit tests πŸ”§: - - Testing the importing of XML data to .parquet and database calls to DuckDB + - Testing the importing of XML data to .duckdb and database calls to DuckDB ## MCP Inspector tests πŸ”: - - Uses the [inspector](https://modelcontextprotocol.io/docs/tools/inspector) provided by anthropic to test connection to the server hosted with streamable http + - Uses the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) provided by Anthropic to test connection to the server hosted with streamable HTTP - Mainly used in the pipeline, but can be run locally ## Opik tests πŸ€–: @@ -40,7 +40,7 @@ npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport ``` - ### Opik tests πŸ€–: -Make sure your `OPIK_WORKSPACE` and `OPIK_API_KEY` environmental variables are set +Make sure your `OPENAI_API_KEY`, `OPIK_WORKSPACE` and `OPIK_API_KEY` environmental variables are set (Opik workspace refers to your profile name and not project name) ```bash uv run tests/opik/tool_calls.py