diff --git a/pyproject.toml b/pyproject.toml index 4c48171..4937299 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,5 +5,10 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ + "agno>=1.7.12", + "ollama>=0.5.3", + "openai>=1.101.0", + "pydantic", + "pyyaml>=6.0.2", "requests>=2.32.5", ] diff --git a/src/agent_system/__init__.py b/src/agent_system/__init__.py new file mode 100644 index 0000000..9f7c533 --- /dev/null +++ b/src/agent_system/__init__.py @@ -0,0 +1,38 @@ +""" +Agent System Module + +基于Agno框架的SubAgent系统实现 +为MedResearcher项目提供智能Agent功能 +""" + +__version__ = "0.1.0" + +# 导入核心类 +from .subagent import SubAgent, create_json_agent +from .config_loader import load_llm_config, get_model_config +from .model_factory import create_agno_model, list_available_models +from .json_processor import parse_json_response, JSONProcessor + +# 导入异常类 +from .subagent import SubAgentError, ConfigurationError, ModelError +from .json_processor import JSONParseError + +__all__ = [ + # 核心类 + 'SubAgent', + 'JSONProcessor', + + # 便捷函数 + 'create_json_agent', + 'load_llm_config', + 'get_model_config', + 'create_agno_model', + 'list_available_models', + 'parse_json_response', + + # 异常类 + 'SubAgentError', + 'ConfigurationError', + 'ModelError', + 'JSONParseError', +] \ No newline at end of file diff --git a/src/agent_system/config_loader.py b/src/agent_system/config_loader.py new file mode 100644 index 0000000..3ecfa01 --- /dev/null +++ b/src/agent_system/config_loader.py @@ -0,0 +1,292 @@ +""" +配置加载模块 + +负责解析LLM配置文件和环境变量,为SubAgent提供统一的配置接口 +""" + +import os +import yaml +from typing import Dict, Any, Optional +from pathlib import Path + + +def load_llm_config(config_path: Optional[str] = None) -> Dict[str, Any]: + """ + 加载LLM配置文件和环境变量 + + Args: + config_path: 配置文件路径,默认为 src/config/llm_config.yaml + + Returns: + 完整的LLM配置字典,包含所有提供商和环境变量 + + Raises: + FileNotFoundError: 配置文件不存在 + yaml.YAMLError: YAML解析错误 + ValueError: 配置格式错误 + """ + + # 确定配置文件路径 + if config_path is None: + # 获取项目根目录 + current_dir = Path(__file__).parent + project_root = current_dir.parent.parent + config_path = project_root / "src" / "config" / "llm_config.yaml" + + config_path = Path(config_path) + + # 检查配置文件是否存在 + if not config_path.exists(): + raise FileNotFoundError(f"LLM配置文件不存在: {config_path}") + + # 读取YAML配置文件 + try: + with open(config_path, 'r', encoding='utf-8') as file: + config = yaml.safe_load(file) + except yaml.YAMLError as e: + raise yaml.YAMLError(f"YAML配置文件解析失败: {e}") + except Exception as e: + raise ValueError(f"读取配置文件失败: {e}") + + # 验证配置结构 + if not isinstance(config, dict): + raise ValueError("配置文件格式错误:根元素必须是字典") + + # 加载环境变量配置 + env_config = load_env_config() + + # 处理环境变量替换 + config = _resolve_env_variables(config, env_config) + + return config + + +def load_env_config(env_path: Optional[str] = None) -> Dict[str, str]: + """ + 加载环境变量配置 + + Args: + env_path: .env文件路径,默认为 src/config/.env + + Returns: + 环境变量字典 + """ + + # 确定环境变量文件路径 + if env_path is None: + current_dir = Path(__file__).parent + project_root = current_dir.parent.parent + env_path = project_root / "src" / "config" / ".env" + + env_config = {} + + # 尝试加载.env文件 + env_path = Path(env_path) + if env_path.exists(): + try: + with open(env_path, 'r', encoding='utf-8') as file: + for line in file: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_config[key.strip()] = value.strip() + except Exception as e: + print(f"警告: 读取.env文件失败: {e}") + + # 同时从系统环境变量中加载 + env_keys = ['DASHSCOPE_API_KEY', 'OPENAI_API_KEY', 'DEEPSEEK_API_KEY'] + for key in env_keys: + if key in os.environ: + env_config[key] = os.environ[key] + + return env_config + + +def _resolve_env_variables(config: Dict[str, Any], env_config: Dict[str, str]) -> Dict[str, Any]: + """ + 解析配置中的环境变量占位符 + + Args: + config: 原始配置 + env_config: 环境变量配置 + + Returns: + 解析后的配置 + """ + + def resolve_value(value): + if isinstance(value, str) and value.startswith('${') and value.endswith('}'): + # 提取环境变量名称 + env_var = value[2:-1] # 去掉 ${ 和 } + + if env_var in env_config: + return env_config[env_var] + elif env_var in os.environ: + return os.environ[env_var] + else: + print(f"警告: 环境变量 {env_var} 未定义,保持原值") + return value + elif isinstance(value, dict): + return {k: resolve_value(v) for k, v in value.items()} + elif isinstance(value, list): + return [resolve_value(item) for item in value] + else: + return value + + return resolve_value(config) + + +def get_provider_config(config: Dict[str, Any], provider: str) -> Dict[str, Any]: + """ + 获取特定提供商的配置 + + Args: + config: 完整的LLM配置 + provider: 提供商名称 (如 'aliyun', 'deepseek', 'openai') + + Returns: + 提供商配置字典 + + Raises: + ValueError: 提供商不存在 + """ + + if provider not in config: + available_providers = list(config.keys()) + raise ValueError(f"提供商 '{provider}' 不存在,可用提供商: {available_providers}") + + return config[provider] + + +def get_model_config(config: Dict[str, Any], provider: str, model_name: str) -> Dict[str, Any]: + """ + 获取特定模型的配置 + + Args: + config: 完整的LLM配置 + provider: 提供商名称 + model_name: 模型名称 + + Returns: + 模型配置字典 + + Raises: + ValueError: 提供商或模型不存在 + """ + + provider_config = get_provider_config(config, provider) + + if 'models' not in provider_config: + raise ValueError(f"提供商 '{provider}' 配置中缺少 models 字段") + + models = provider_config['models'] + + if model_name not in models: + available_models = list(models.keys()) + raise ValueError(f"模型 '{model_name}' 在提供商 '{provider}' 中不存在,可用模型: {available_models}") + + model_config = models[model_name].copy() + + # 添加提供商级别的配置 + for key in ['base_url', 'api_key']: + if key in provider_config: + model_config[key] = provider_config[key] + + return model_config + + +def validate_config(config: Dict[str, Any]) -> bool: + """ + 验证配置文件的完整性 + + Args: + config: LLM配置字典 + + Returns: + 验证是否通过 + """ + + required_providers = ['aliyun', 'deepseek', 'openai'] + missing_providers = [] + + for provider in required_providers: + if provider not in config: + missing_providers.append(provider) + + if missing_providers: + print(f"警告: 缺少提供商配置: {missing_providers}") + + # 验证每个提供商的配置结构 + valid = True + for provider_name, provider_config in config.items(): + if not isinstance(provider_config, dict): + print(f"错误: 提供商 '{provider_name}' 配置必须是字典") + valid = False + continue + + if 'models' not in provider_config: + print(f"错误: 提供商 '{provider_name}' 缺少 models 字段") + valid = False + continue + + models = provider_config['models'] + if not isinstance(models, dict) or not models: + print(f"错误: 提供商 '{provider_name}' 的 models 必须是非空字典") + valid = False + continue + + # 验证每个模型配置 + for model_name, model_config in models.items(): + if not isinstance(model_config, dict): + print(f"错误: 模型 '{model_name}' 配置必须是字典") + valid = False + continue + + required_fields = ['class', 'params'] + for field in required_fields: + if field not in model_config: + print(f"错误: 模型 '{model_name}' 缺少必需字段: {field}") + valid = False + + return valid + + +# 测试函数 +def test_config_loading(): + """测试配置加载功能""" + try: + print("正在测试配置加载...") + + # 加载配置 + config = load_llm_config() + print(f"✅ 配置加载成功,找到 {len(config)} 个提供商") + + # 验证配置 + is_valid = validate_config(config) + print(f"✅ 配置验证: {'通过' if is_valid else '失败'}") + + # 测试提供商配置获取 + try: + aliyun_config = get_provider_config(config, 'aliyun') + print(f"✅ 阿里云配置获取成功,包含 {len(aliyun_config.get('models', {}))} 个模型") + except ValueError as e: + print(f"❌ 阿里云配置获取失败: {e}") + + # 测试模型配置获取 + try: + qwen_config = get_model_config(config, 'aliyun', 'qwen-max') + print(f"✅ qwen-max模型配置获取成功") + print(f" 模型类: {qwen_config.get('class', 'N/A')}") + print(f" 模型ID: {qwen_config.get('params', {}).get('id', 'N/A')}") + except ValueError as e: + print(f"❌ qwen-max模型配置获取失败: {e}") + + return True + + except Exception as e: + print(f"❌ 配置加载测试失败: {e}") + return False + + +if __name__ == "__main__": + test_config_loading() \ No newline at end of file diff --git a/src/agent_system/json_processor.py b/src/agent_system/json_processor.py new file mode 100644 index 0000000..dfc6a1b --- /dev/null +++ b/src/agent_system/json_processor.py @@ -0,0 +1,404 @@ +""" +JSON处理器模块 + +提供强大的JSON解析和验证功能,支持零容错解析和Pydantic模型集成 +""" + +import json +import re +from typing import Any, Dict, List, Optional, Type, Union, get_origin, get_args +from pydantic import BaseModel, ValidationError + + +class JSONParseError(Exception): + """JSON解析错误""" + pass + + +class JSONProcessor: + """ + JSON处理器类 + + 提供多种JSON解析策略,确保即使在不完美的JSON输出下也能成功解析 + """ + + def __init__(self): + """初始化JSON处理器""" + pass + + def parse_json_response( + self, + response: str, + response_model: Optional[Type[BaseModel]] = None + ) -> Union[Dict, List, BaseModel]: + """ + 解析JSON响应 + + Args: + response: 响应字符串 + response_model: 可选的Pydantic模型类 + + Returns: + 解析后的数据对象或模型实例 + + Raises: + JSONParseError: 解析失败时抛出 + """ + + if not response or not response.strip(): + raise JSONParseError("响应为空,无法解析JSON") + + # 清理响应字符串 + cleaned_response = self._clean_response(response) + + # 尝试多种解析策略 + parsed_data = self._try_multiple_parsing_strategies(cleaned_response) + + if parsed_data is None: + raise JSONParseError(f"所有解析策略都失败了,响应内容: {response[:200]}...") + + # 如果指定了响应模型,尝试创建模型实例 + if response_model is not None: + return self._create_model_instance(parsed_data, response_model) + + return parsed_data + + def _clean_response(self, response: str) -> str: + """ + 清理响应字符串 + + Args: + response: 原始响应 + + Returns: + 清理后的响应 + """ + + # 去除首尾空白 + cleaned = response.strip() + + # 移除可能的markdown代码块标记 + if cleaned.startswith('```json'): + cleaned = cleaned[7:] + elif cleaned.startswith('```'): + cleaned = cleaned[3:] + + if cleaned.endswith('```'): + cleaned = cleaned[:-3] + + # 移除可能的额外引号包装 + if cleaned.startswith('"') and cleaned.endswith('"'): + try: + # 尝试解析为字符串,看是否是被引号包装的JSON + unquoted = json.loads(cleaned) + if isinstance(unquoted, str): + cleaned = unquoted + except json.JSONDecodeError: + pass + + return cleaned.strip() + + def _try_multiple_parsing_strategies(self, response: str) -> Optional[Union[Dict, List]]: + """ + 尝试多种解析策略 + + Args: + response: 清理后的响应 + + Returns: + 解析成功的数据,或None + """ + + strategies = [ + self._strategy_direct_parse, + self._strategy_extract_json_block, + self._strategy_find_json_structure, + self._strategy_regex_extract, + self._strategy_fix_common_errors, + ] + + for strategy in strategies: + try: + result = strategy(response) + if result is not None: + return result + except Exception: + continue + + return None + + def _strategy_direct_parse(self, response: str) -> Optional[Union[Dict, List]]: + """策略1: 直接解析JSON""" + try: + return json.loads(response) + except json.JSONDecodeError: + return None + + def _strategy_extract_json_block(self, response: str) -> Optional[Union[Dict, List]]: + """策略2: 提取JSON代码块""" + # 查找被代码块包围的JSON + code_block_pattern = r'```(?:json)?\s*([\s\S]*?)\s*```' + matches = re.findall(code_block_pattern, response, re.IGNORECASE) + + for match in matches: + try: + return json.loads(match.strip()) + except json.JSONDecodeError: + continue + + return None + + def _strategy_find_json_structure(self, response: str) -> Optional[Union[Dict, List]]: + """策略3: 查找JSON结构""" + # 查找第一个完整的JSON对象或数组 + for start_char, end_char in [("{", "}"), ("[", "]")]: + json_str = self._extract_complete_json_structure(response, start_char, end_char) + if json_str: + try: + return json.loads(json_str) + except json.JSONDecodeError: + continue + + return None + + def _strategy_regex_extract(self, response: str) -> Optional[Union[Dict, List]]: + """策略4: 正则表达式提取""" + # 使用正则表达式查找JSON模式 + patterns = [ + r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', # 嵌套对象 + r'\[[^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*\]', # 嵌套数组 + ] + + for pattern in patterns: + matches = re.findall(pattern, response) + for match in matches: + try: + return json.loads(match) + except json.JSONDecodeError: + continue + + return None + + def _strategy_fix_common_errors(self, response: str) -> Optional[Union[Dict, List]]: + """策略5: 修复常见JSON错误""" + # 修复常见的JSON格式错误 + fixed_response = response + + # 修复单引号为双引号 + fixed_response = re.sub(r"'([^']*)':", r'"\1":', fixed_response) + fixed_response = re.sub(r":\s*'([^']*)'", r': "\1"', fixed_response) + + # 修复末尾逗号 + fixed_response = re.sub(r',(\s*[}\]])', r'\1', fixed_response) + + # 修复未引用的键 + fixed_response = re.sub(r'(\w+):', r'"\1":', fixed_response) + + try: + return json.loads(fixed_response) + except json.JSONDecodeError: + return None + + def _extract_complete_json_structure( + self, + text: str, + start_char: str, + end_char: str + ) -> Optional[str]: + """ + 提取完整的JSON结构 + + Args: + text: 文本内容 + start_char: 开始字符 ('{' 或 '[') + end_char: 结束字符 ('}' 或 ']') + + Returns: + 提取的JSON字符串或None + """ + + start_idx = text.find(start_char) + if start_idx == -1: + return None + + # 使用计数器匹配嵌套结构 + count = 0 + in_string = False + escape_next = False + + for i, char in enumerate(text[start_idx:], start_idx): + if escape_next: + escape_next = False + continue + + if char == '\\' and in_string: + escape_next = True + continue + + if char == '"' and not escape_next: + in_string = not in_string + continue + + if not in_string: + if char == start_char: + count += 1 + elif char == end_char: + count -= 1 + + if count == 0: + return text[start_idx:i+1] + + # 如果没有找到匹配的结束符,返回到字符串末尾 + if count > 0: + return text[start_idx:] + + return None + + def _create_model_instance( + self, + data: Union[Dict, List], + response_model: Type[BaseModel] + ) -> BaseModel: + """ + 创建Pydantic模型实例 + + Args: + data: 解析后的数据 + response_model: Pydantic模型类 + + Returns: + 模型实例 + + Raises: + JSONParseError: 模型验证失败 + """ + + try: + # 如果数据是列表但模型期望对象,尝试包装 + if isinstance(data, list) and not self._is_list_model(response_model): + # 尝试将列表作为某个字段的值 + field_names = list(response_model.model_fields.keys()) + if field_names: + # 使用第一个字段名作为包装器 + data = {field_names[0]: data} + + # 创建模型实例 + return response_model(**data) + + except ValidationError as e: + # 详细的验证错误信息 + error_msg = f"Pydantic模型验证失败: {e}" + raise JSONParseError(error_msg) + except TypeError as e: + error_msg = f"模型实例化失败: {e}" + raise JSONParseError(error_msg) + + def _is_list_model(self, model: Type[BaseModel]) -> bool: + """ + 检查模型是否期望列表类型 + + Args: + model: Pydantic模型类 + + Returns: + 是否为列表模型 + """ + + # 检查模型的字段类型 + for field_info in model.model_fields.values(): + annotation = field_info.annotation + origin = get_origin(annotation) + + # 如果有字段是List类型,认为这是一个列表模型 + if origin is list or origin is List: + return True + + return False + + def validate_json_schema(self, data: Dict, schema: Dict) -> bool: + """ + 验证JSON数据是否符合指定的schema + + Args: + data: JSON数据 + schema: JSON schema + + Returns: + 是否符合schema + """ + + try: + import jsonschema + jsonschema.validate(data, schema) + return True + except ImportError: + # 如果没有安装jsonschema库,跳过验证 + return True + except Exception: + return False + + +# 便捷函数 +def parse_json_response( + response: str, + response_model: Optional[Type[BaseModel]] = None +) -> Union[Dict, List, BaseModel]: + """ + 便捷函数:解析JSON响应 + + Args: + response: 响应字符串 + response_model: 可选的Pydantic模型类 + + Returns: + 解析后的数据或模型实例 + """ + processor = JSONProcessor() + return processor.parse_json_response(response, response_model) + + +# 测试函数 +def test_json_processor(): + """测试JSON处理器功能""" + print("正在测试JSON处理器...") + + processor = JSONProcessor() + + # 测试用例 + test_cases = [ + # 标准JSON + ('{"name": "test", "value": 123}', None), + + # 带代码块标记的JSON + ('```json\n{"name": "test", "value": 123}\n```', None), + + # 不完整的JSON + ('{"name": "test", "value": 123', None), + + # 单引号JSON + ("{'name': 'test', 'value': 123}", None), + + # 包含额外文本的JSON + ('这是一个JSON响应: {"name": "test", "value": 123} 解析完成', None), + + # 数组JSON + ('[{"name": "test1"}, {"name": "test2"}]', None), + ] + + success_count = 0 + + for i, (test_input, model) in enumerate(test_cases, 1): + try: + result = processor.parse_json_response(test_input, model) + print(f"✅ 测试用例 {i}: 解析成功 - {type(result)}") + success_count += 1 + except Exception as e: + print(f"❌ 测试用例 {i}: 解析失败 - {e}") + + print(f"\n🎯 JSON处理器测试完成: {success_count}/{len(test_cases)} 通过") + + return success_count == len(test_cases) + + +if __name__ == "__main__": + test_json_processor() \ No newline at end of file diff --git a/src/agent_system/model_factory.py b/src/agent_system/model_factory.py new file mode 100644 index 0000000..3303530 --- /dev/null +++ b/src/agent_system/model_factory.py @@ -0,0 +1,312 @@ +""" +模型工厂模块 + +基于配置创建各种Agno模型实例,支持多种LLM提供商 +""" + +from typing import Dict, Any, Optional, Union +from agno.models.openai import OpenAILike, OpenAIChat + +# 尝试导入其他可选模型 +try: + from agno.models.ollama import Ollama + OLLAMA_AVAILABLE = True +except ImportError: + OLLAMA_AVAILABLE = False + Ollama = None + +from .config_loader import load_llm_config, get_model_config + + +class ModelFactory: + """ + 模型工厂类 + + 负责根据配置创建不同类型的Agno模型实例 + """ + + # 支持的模型类映射 + MODEL_CLASSES = { + "OpenAILike": OpenAILike, + "OpenAIChat": OpenAIChat, + } + + def __init__(self, config: Optional[Dict[str, Any]] = None): + """ + 初始化模型工厂 + + Args: + config: LLM配置,如果为None则自动加载 + """ + self.config = config if config is not None else load_llm_config() + + # 如果Ollama可用,添加到支持列表 + if OLLAMA_AVAILABLE: + self.MODEL_CLASSES["Ollama"] = Ollama + + def create_model( + self, + provider: str, + model_name: str, + **override_params + ) -> Union[OpenAILike, OpenAIChat]: + """ + 创建模型实例 + + Args: + provider: 提供商名称 (如 'aliyun', 'deepseek', 'openai') + model_name: 模型名称 (如 'qwen-max', 'deepseek-v3') + **override_params: 覆盖配置的参数 + + Returns: + 创建的模型实例 + + Raises: + ValueError: 配置错误或不支持的模型类型 + ImportError: 缺少必需的依赖 + """ + + # 获取模型配置 + model_config = get_model_config(self.config, provider, model_name) + + # 获取模型类名 + model_class_name = model_config.get('class') + if not model_class_name: + raise ValueError(f"模型配置中缺少 'class' 字段: {provider}.{model_name}") + + # 检查模型类是否支持 + if model_class_name not in self.MODEL_CLASSES: + available_classes = list(self.MODEL_CLASSES.keys()) + raise ValueError(f"不支持的模型类 '{model_class_name}',支持的类型: {available_classes}") + + # 特殊处理Ollama + if model_class_name == "Ollama" and not OLLAMA_AVAILABLE: + raise ImportError("Ollama模型需要安装ollama包: pip install ollama") + + # 获取模型类 + model_class = self.MODEL_CLASSES[model_class_name] + + # 准备初始化参数 + init_params = self._prepare_init_params(model_config, model_class_name, override_params) + + # 创建模型实例 + try: + model = model_class(**init_params) + return model + except Exception as e: + raise ValueError(f"创建模型实例失败 ({provider}.{model_name}): {e}") + + def _prepare_init_params( + self, + model_config: Dict[str, Any], + model_class_name: str, + override_params: Dict[str, Any] + ) -> Dict[str, Any]: + """ + 准备模型初始化参数 + + Args: + model_config: 模型配置 + model_class_name: 模型类名 + override_params: 覆盖参数 + + Returns: + 初始化参数字典 + """ + + # 基础参数从params字段获取 + init_params = model_config.get('params', {}).copy() + + # 添加API相关参数 + if 'api_key' in model_config: + init_params['api_key'] = model_config['api_key'] + + if 'base_url' in model_config: + init_params['base_url'] = model_config['base_url'] + + # 根据模型类型调整参数 + if model_class_name == "OpenAILike": + # OpenAILike模型特殊处理 + self._adjust_openai_like_params(init_params) + elif model_class_name == "OpenAIChat": + # OpenAIChat模型特殊处理 + self._adjust_openai_chat_params(init_params) + elif model_class_name == "Ollama": + # Ollama模型特殊处理 + self._adjust_ollama_params(init_params) + + # 应用覆盖参数 + init_params.update(override_params) + + return init_params + + def _adjust_openai_like_params(self, params: Dict[str, Any]) -> None: + """调整OpenAILike模型参数""" + # OpenAILike使用id参数,不需要model参数 + # id参数已经在params中,无需额外处理 + pass + + def _adjust_openai_chat_params(self, params: Dict[str, Any]) -> None: + """调整OpenAIChat模型参数""" + # OpenAIChat通常需要id参数作为model参数 + if 'id' in params and 'model' not in params: + params['model'] = params['id'] + + def _adjust_ollama_params(self, params: Dict[str, Any]) -> None: + """调整Ollama模型参数""" + # Ollama模型通常不需要api_key和base_url + params.pop('api_key', None) + params.pop('base_url', None) + + # 使用id作为model参数 + if 'id' in params and 'model' not in params: + params['model'] = params['id'] + + def list_available_models(self) -> Dict[str, list]: + """ + 列出所有可用的模型 + + Returns: + 按提供商分组的模型列表 + """ + available_models = {} + + for provider, provider_config in self.config.items(): + if 'models' in provider_config: + models = list(provider_config['models'].keys()) + available_models[provider] = models + + return available_models + + def validate_model_exists(self, provider: str, model_name: str) -> bool: + """ + 验证模型是否存在 + + Args: + provider: 提供商名称 + model_name: 模型名称 + + Returns: + 模型是否存在 + """ + try: + get_model_config(self.config, provider, model_name) + return True + except ValueError: + return False + + +# 便捷函数 +def create_agno_model( + provider: str, + model_name: str, + config: Optional[Dict[str, Any]] = None, + **override_params +) -> Union[OpenAILike, OpenAIChat]: + """ + 便捷函数:创建Agno模型实例 + + Args: + provider: 提供商名称 + model_name: 模型名称 + config: 可选的配置字典 + **override_params: 覆盖参数 + + Returns: + 创建的模型实例 + """ + factory = ModelFactory(config) + return factory.create_model(provider, model_name, **override_params) + + +def list_available_models(config: Optional[Dict[str, Any]] = None) -> Dict[str, list]: + """ + 便捷函数:列出可用模型 + + Args: + config: 可选的配置字典 + + Returns: + 按提供商分组的模型列表 + """ + factory = ModelFactory(config) + return factory.list_available_models() + + +# 测试函数 +def test_model_creation(): + """测试模型创建功能""" + print("正在测试模型创建...") + + try: + # 创建模型工厂 + factory = ModelFactory() + + # 列出可用模型 + available_models = factory.list_available_models() + print(f"✅ 发现可用模型:") + for provider, models in available_models.items(): + print(f" {provider}: {models}") + + # 测试阿里云qwen-max模型创建 + try: + qwen_model = factory.create_model('aliyun', 'qwen-max') + print(f"✅ qwen-max模型创建成功: {type(qwen_model)}") + except Exception as e: + print(f"❌ qwen-max模型创建失败: {e}") + + # 测试DeepSeek模型创建 + try: + deepseek_model = factory.create_model('deepseek', 'deepseek-v3') + print(f"✅ deepseek-v3模型创建成功: {type(deepseek_model)}") + except Exception as e: + print(f"❌ deepseek-v3模型创建失败: {e}") + + # 测试参数覆盖 + try: + custom_model = factory.create_model( + 'aliyun', + 'qwen-plus', + temperature=0.5, + max_tokens=1000 + ) + print(f"✅ 参数覆盖测试成功: {type(custom_model)}") + except Exception as e: + print(f"❌ 参数覆盖测试失败: {e}") + + return True + + except Exception as e: + print(f"❌ 模型创建测试失败: {e}") + return False + + +def test_convenience_functions(): + """测试便捷函数""" + print("\n正在测试便捷函数...") + + try: + # 测试便捷创建函数 + model = create_agno_model('aliyun', 'qwen-turbo') + print(f"✅ 便捷函数创建模型成功: {type(model)}") + + # 测试列出模型函数 + models = list_available_models() + total_models = sum(len(model_list) for model_list in models.values()) + print(f"✅ 便捷函数列出模型成功,总计 {total_models} 个模型") + + return True + + except Exception as e: + print(f"❌ 便捷函数测试失败: {e}") + return False + + +if __name__ == "__main__": + success1 = test_model_creation() + success2 = test_convenience_functions() + + if success1 and success2: + print("\n🎉 所有模型工厂测试通过!") + else: + print("\n💥 部分测试失败,请检查配置") \ No newline at end of file diff --git a/src/agent_system/subagent.py b/src/agent_system/subagent.py new file mode 100644 index 0000000..a2b697f --- /dev/null +++ b/src/agent_system/subagent.py @@ -0,0 +1,434 @@ +""" +SubAgent核心类 + +基于Agno框架的智能代理实现,提供动态prompt构建、JSON解析、模型管理等功能 +""" + +import logging +from typing import Any, Dict, List, Optional, Type, Union +from agno.agent import Agent, RunResponse +from pydantic import BaseModel + +from .config_loader import load_llm_config +from .model_factory import ModelFactory +from .json_processor import JSONProcessor, JSONParseError + +# 设置日志 +logger = logging.getLogger(__name__) + + +class SubAgentError(Exception): + """SubAgent相关错误的基类""" + pass + + +class ConfigurationError(SubAgentError): + """配置相关错误""" + pass + + +class ModelError(SubAgentError): + """模型相关错误""" + pass + + +class SubAgent: + """ + SubAgent核心类 + + 基于Agno框架构建的智能代理,支持: + - 动态prompt模板构建 + - 多LLM提供商支持 + - JSON结构化输出 + - 零容错解析 + - 灵活的配置管理 + """ + + def __init__( + self, + provider: str, + model_name: str, + instructions: Optional[List[str]] = None, + name: Optional[str] = None, + description: Optional[str] = None, + prompt_template: Optional[str] = None, + response_model: Optional[Type[BaseModel]] = None, + config: Optional[Dict[str, Any]] = None, + **agent_kwargs + ): + """ + 初始化SubAgent + + Args: + provider: LLM提供商名称 (如 'aliyun', 'deepseek', 'openai') + model_name: 模型名称 (如 'qwen-max', 'deepseek-v3') + instructions: 指令列表 + name: Agent名称 + description: Agent描述 + prompt_template: 动态prompt模板 + response_model: Pydantic响应模型类 + config: 自定义LLM配置 + **agent_kwargs: 传递给Agno Agent的额外参数 + + Raises: + ConfigurationError: 配置错误 + ModelError: 模型创建错误 + """ + + # 基础属性设置 + self.provider = provider + self.model_name = model_name + self.name = name or f"{provider}_{model_name}_agent" + self.description = description or f"基于{provider}的{model_name}模型的智能代理" + self.instructions = instructions or [] + self.prompt_template = prompt_template + self.response_model = response_model + + # 初始化组件 + try: + # 加载配置 + self.config = config if config is not None else load_llm_config() + + # 创建模型工厂和JSON处理器 + self.model_factory = ModelFactory(self.config) + self.json_processor = JSONProcessor() + + # 创建Agno模型 + self.model = self._create_model() + + # 创建Agno Agent + self.agent = self._create_agent(**agent_kwargs) + + logger.info(f"SubAgent {self.name} 初始化成功") + + except Exception as e: + logger.error(f"SubAgent初始化失败: {e}") + raise ConfigurationError(f"SubAgent初始化失败: {e}") + + def _create_model(self): + """创建Agno模型实例""" + try: + return self.model_factory.create_model(self.provider, self.model_name) + except Exception as e: + raise ModelError(f"模型创建失败: {e}") + + def _create_agent(self, **agent_kwargs): + """创建Agno Agent实例""" + try: + agent_params = { + "model": self.model, + "name": self.name, + "description": self.description, + "instructions": self.instructions, + "markdown": agent_kwargs.pop("markdown", True), + "debug_mode": agent_kwargs.pop("debug_mode", False), + **agent_kwargs + } + + return Agent(**agent_params) + + except Exception as e: + raise ConfigurationError(f"Agent创建失败: {e}") + + def build_prompt(self, template_vars: Optional[Dict[str, Any]] = None) -> str: + """ + 构建动态prompt + + Args: + template_vars: 模板变量字典,用于替换模板中的占位符 + + Returns: + 构建完成的prompt字符串 + + Raises: + SubAgentError: prompt构建失败 + """ + + if not self.prompt_template: + raise SubAgentError("未设置prompt模板,无法构建动态prompt") + + if not template_vars: + template_vars = {} + + try: + # 使用str.format()进行模板替换 + prompt = self.prompt_template.format(**template_vars) + + # 如果需要JSON输出,添加JSON格式要求 + if self.response_model: + json_instruction = self._build_json_instruction() + prompt = f"{prompt}\n\n{json_instruction}" + + return prompt + + except KeyError as e: + raise SubAgentError(f"模板变量缺失: {e}") + except Exception as e: + raise SubAgentError(f"prompt构建失败: {e}") + + def _build_json_instruction(self) -> str: + """构建JSON格式指令""" + json_instruction = """ +请严格按照以下JSON格式返回结果,不要添加任何额外的文字说明: + +""" + + if self.response_model: + # 生成JSON schema示例 + try: + schema = self.response_model.model_json_schema() + json_instruction += f"JSON Schema:\n{schema}\n\n" + + # 生成示例 + example = self._generate_model_example() + if example: + json_instruction += f"示例格式:\n{example}" + + except Exception: + # 如果schema生成失败,使用通用指令 + json_instruction += "请返回有效的JSON格式数据。" + + return json_instruction + + def _generate_model_example(self) -> Optional[str]: + """生成Pydantic模型的示例JSON""" + try: + # 创建一个示例实例 + field_values = {} + for field_name, field_info in self.response_model.model_fields.items(): + # 根据字段类型生成示例值 + field_type = field_info.annotation + field_values[field_name] = self._generate_example_value(field_type) + + example_instance = self.response_model(**field_values) + return example_instance.model_dump_json(indent=2) + + except Exception: + return None + + def _generate_example_value(self, field_type: Type) -> Any: + """根据字段类型生成示例值""" + if field_type == str: + return "示例文本" + elif field_type == int: + return 0 + elif field_type == float: + return 0.0 + elif field_type == bool: + return True + elif hasattr(field_type, '__origin__'): + origin = field_type.__origin__ + if origin == list: + return [] + elif origin == dict: + return {} + + return "示例值" + + def run( + self, + prompt: Optional[str] = None, + template_vars: Optional[Dict[str, Any]] = None, + **run_kwargs + ) -> Any: + """ + 执行Agent推理 + + Args: + prompt: 直接prompt文本(如果提供,将忽略template_vars) + template_vars: 模板变量(用于动态构建prompt) + **run_kwargs: 传递给Agent.run的额外参数 + + Returns: + 如果指定了response_model,返回解析后的Pydantic模型实例 + 否则返回原始响应内容 + + Raises: + SubAgentError: 执行失败 + JSONParseError: JSON解析失败 + """ + + try: + # 构建最终prompt + if prompt is not None: + final_prompt = prompt + else: + final_prompt = self.build_prompt(template_vars) + + logger.debug(f"Agent {self.name} 执行推理,prompt长度: {len(final_prompt)}") + + # 执行Agent推理 + response: RunResponse = self.agent.run(final_prompt, **run_kwargs) + + # 获取响应内容 + content = response.content + + if self.response_model: + # 如果指定了响应模型,进行JSON解析 + return self._parse_structured_response(content) + else: + # 返回原始内容 + return content + + except JSONParseError: + # JSON解析错误直接抛出 + raise + except Exception as e: + logger.error(f"Agent执行失败: {e}") + raise SubAgentError(f"Agent执行失败: {e}") + + def _parse_structured_response(self, content: str) -> BaseModel: + """解析结构化响应""" + try: + return self.json_processor.parse_json_response(content, self.response_model) + except JSONParseError as e: + logger.error(f"JSON解析失败: {e}") + logger.debug(f"响应内容: {content}") + raise + + def get_model_info(self) -> Dict[str, Any]: + """ + 获取模型信息 + + Returns: + 包含模型信息的字典 + """ + return { + "name": self.name, + "provider": self.provider, + "model_name": self.model_name, + "model_type": type(self.model).__name__, + "has_prompt_template": self.prompt_template is not None, + "has_response_model": self.response_model is not None, + "instructions_count": len(self.instructions), + } + + def update_instructions(self, instructions: List[str]) -> None: + """ + 更新指令列表 + + Args: + instructions: 新的指令列表 + """ + self.instructions = instructions + # 注意:这里不会更新已创建的Agent实例,如需更新需要重新创建Agent + logger.info(f"Agent {self.name} 指令已更新") + + def update_prompt_template(self, template: str) -> None: + """ + 更新prompt模板 + + Args: + template: 新的prompt模板 + """ + self.prompt_template = template + logger.info(f"Agent {self.name} prompt模板已更新") + + def __str__(self) -> str: + return f"SubAgent({self.name}, {self.provider}.{self.model_name})" + + def __repr__(self) -> str: + return self.__str__() + + +# 便捷函数 +def create_json_agent( + provider: str, + model_name: str, + name: str, + prompt_template: str, + response_model: Union[str, Type[BaseModel]], + instructions: Optional[List[str]] = None, + **kwargs +) -> SubAgent: + """ + 便捷函数:创建支持JSON输出的SubAgent + + Args: + provider: 提供商名称 + model_name: 模型名称 + name: Agent名称 + prompt_template: prompt模板 + response_model: 响应模型类或模块路径字符串 + instructions: 指令列表 + **kwargs: 额外参数 + + Returns: + 配置好的SubAgent实例 + """ + + # 如果response_model是字符串,尝试导入 + if isinstance(response_model, str): + try: + # 解析模块路径和类名 + if '.' in response_model: + module_path, class_name = response_model.rsplit('.', 1) + else: + # 如果没有模块路径,假设在当前模块 + module_path = '__main__' + class_name = response_model + + # 动态导入 + import importlib + module = importlib.import_module(module_path) + response_model = getattr(module, class_name) + + except Exception as e: + raise ValueError(f"无法导入响应模型 {response_model}: {e}") + + return SubAgent( + provider=provider, + model_name=model_name, + name=name, + prompt_template=prompt_template, + response_model=response_model, + instructions=instructions, + **kwargs + ) + + +# 测试函数 +def test_subagent(): + """测试SubAgent功能""" + print("正在测试SubAgent...") + + try: + # 创建基础SubAgent + agent = SubAgent( + provider="aliyun", + model_name="qwen-turbo", + name="test_agent", + instructions=["你是一个测试助手", "请简洁回答问题"] + ) + + print(f"✅ SubAgent创建成功: {agent}") + print(f" 模型信息: {agent.get_model_info()}") + + # 测试基础对话 + try: + response = agent.run("请简单介绍一下Python语言") + print(f"✅ 基础对话测试成功,响应长度: {len(str(response))}字符") + except Exception as e: + print(f"❌ 基础对话测试失败: {e}") + + # 测试动态prompt构建 + agent.update_prompt_template("请回答关于{topic}的问题:{question}") + + try: + prompt = agent.build_prompt({ + "topic": "编程", + "question": "什么是函数?" + }) + print(f"✅ 动态prompt构建成功,长度: {len(prompt)}字符") + except Exception as e: + print(f"❌ 动态prompt构建失败: {e}") + + return True + + except Exception as e: + print(f"❌ SubAgent测试失败: {e}") + return False + + +if __name__ == "__main__": + test_subagent() \ No newline at end of file diff --git a/src/config/.env b/src/config/.env new file mode 100644 index 0000000..0dbb509 --- /dev/null +++ b/src/config/.env @@ -0,0 +1 @@ +DASHSCOPE_API_KEY=sk-5c7f9dc33e0a43738d415a0432452b93 diff --git a/src/config/llm_config.yaml b/src/config/llm_config.yaml new file mode 100644 index 0000000..bad8dc8 --- /dev/null +++ b/src/config/llm_config.yaml @@ -0,0 +1,93 @@ +# LLM配置文件 +# 定义所有Agent可用的LLM模型配置 + + +# 阿里云通义千问配置 +aliyun: + base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1" + api_key: ${DASHSCOPE_API_KEY} # 从环境变量读取 + models: + qwen-max: + class: "OpenAILike" + params: + id: "qwen-max" + temperature: 0.7 + max_tokens: 3000 + qwen-plus: + class: "OpenAILike" + params: + id: "qwen-plus" + temperature: 0.7 + max_tokens: 2000 + qwen-turbo: + class: "OpenAILike" + params: + id: "qwen-turbo" + temperature: 0.7 + max_tokens: 1500 + +# DeepSeek配置 +deepseek: + base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1" + api_key: ${DASHSCOPE_API_KEY} + models: + deepseek-v3: + class: "OpenAILike" + params: + id: "deepseek-v3" + temperature: 0.7 + max_tokens: 4000 + deepseek-r1: + class: "OpenAILike" + params: + id: "deepseek-r1" + temperature: 0.5 + max_tokens: 8000 + # DeepSeek R1特有的推理模式 + reasoning_effort: "high" + +# OpenAI配置(备用) +openai: + base_url: "https://api.openai.com/v1" + api_key: ${OPENAI_API_KEY} + models: + gpt-4: + class: "OpenAIChat" + params: + id: "gpt-4" + temperature: 0.7 + max_tokens: 4000 + gpt-3.5-turbo: + class: "OpenAIChat" + params: + id: "gpt-3.5-turbo" + temperature: 0.7 + max_tokens: 2000 + +# 本地Ollama配置(开发测试用) +ollama: + host: "127.0.0.1" + port: 11434 + models: + qwen2.5: + class: "Ollama" + params: + id: "qwen2.5:latest" + temperature: 0.7 + max_tokens: 2000 + llama3: + class: "Ollama" + params: + id: "llama3:latest" + temperature: 0.7 + max_tokens: 2000 + +vllm: + base_url: "http://100.82.33.121:11001/v1" + models: + gpt-oss: + class: "OpenAIChat" + params: + id: "gpt-oss-20b" + temperature: 0.7 + max_tokens: 2000 \ No newline at end of file diff --git a/uv.lock b/uv.lock index 92eaba8..639fa79 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,52 @@ version = 1 revision = 2 requires-python = ">=3.13" +[[package]] +name = "agno" +version = "1.7.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docstring-parser" }, + { name = "gitpython" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tomli" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/4c/5a1b7b7bc41c521fe4659e15e2594801ae5356444ec29ffb1e7227f47428/agno-1.7.12.tar.gz", hash = "sha256:df0627cbb66d7d73ec639d23821a549e9a4e6178f812dceaf000697d6246b9d8", size = 747380, upload-time = "2025-08-20T20:47:16.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/53/f8d7a8d5e18d9cf1362b9a9c54ba1895625d7b65aa9e9487fc5439aca5b7/agno-1.7.12-py3-none-any.whl", hash = "sha256:288cf2e1cf4df2496bc8c44ab1ae28ea7821a090d00d3eca92f521fc8bbe3c13", size = 988853, upload-time = "2025-08-20T20:47:13.457Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -42,6 +88,106 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "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 = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -51,16 +197,227 @@ 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 = "jiter" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" }, + { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" }, + { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" }, + { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" }, + { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" }, + { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" }, + { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" }, + { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "medresearcher" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "agno" }, + { name = "ollama" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pyyaml" }, { name = "requests" }, ] [package.metadata] -requires-dist = [{ name = "requests", specifier = ">=2.32.5" }] +requires-dist = [ + { name = "agno", specifier = ">=1.7.12" }, + { name = "ollama", specifier = ">=0.5.3" }, + { name = "openai", specifier = ">=1.101.0" }, + { name = "pydantic" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "requests", specifier = ">=2.32.5" }, +] + +[[package]] +name = "ollama" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/6d/ae96027416dcc2e98c944c050c492789502d7d7c0b95a740f0bb39268632/ollama-0.5.3.tar.gz", hash = "sha256:40b6dff729df3b24e56d4042fd9d37e231cee8e528677e0d085413a1d6692394", size = 43331, upload-time = "2025-08-07T21:44:10.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/f6/2091e50b8b6c3e6901f6eab283d5efd66fb71c86ddb1b4d68766c3eeba0f/ollama-0.5.3-py3-none-any.whl", hash = "sha256:a8303b413d99a9043dbf77ebf11ced672396b59bec27e6d5db67c88f01b279d2", size = 13490, upload-time = "2025-08-07T21:44:09.353Z" }, +] + +[[package]] +name = "openai" +version = "1.101.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/00/7c/eaf06b62281f5ca4f774c4cff066e6ddfd6a027e0ac791be16acec3a95e3/openai-1.101.0.tar.gz", hash = "sha256:29f56df2236069686e64aca0e13c24a4ec310545afb25ef7da2ab1a18523f22d", size = 518415, upload-time = "2025-08-21T21:11:01.645Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/a6/0e39baa335bbd1c66c7e0a41dbbec10c5a15ab95c1344e7f7beb28eee65a/openai-1.101.0-py3-none-any.whl", hash = "sha256:6539a446cce154f8d9fb42757acdfd3ed9357ab0d34fcac11096c461da87133b", size = 810772, upload-time = "2025-08-21T21:10:59.215Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { 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-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { 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 = "requests" @@ -77,6 +434,113 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[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 = "typer" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/78/d90f616bf5f88f8710ad067c1f8705bf7618059836ca084e5bb2a0855d75/typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614", size = 102836, upload-time = "2025-08-18T19:18:22.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/76/06dbe78f39b2203d2a47d5facc5df5102d0561e2807396471b5f7c5a30a1/typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9", size = 46397, upload-time = "2025-08-18T19:18:21.663Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + [[package]] name = "urllib3" version = "2.5.0"