diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..e5a06cd --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,39 @@ +name: Code Quality + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + +jobs: + code-quality: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install UV + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: | + uv sync --extra dev + + - name: Run Ruff (Linting) + run: | + uv run ruff check . + + - name: Run Ruff (Format Check) + run: | + uv run ruff format --check . \ No newline at end of file diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..50394d2 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,56 @@ +name: Pre-commit Checks + +on: + push: + pull_request: + +jobs: + pre-commit: + runs-on: ubuntu-latest + name: Code Quality Gate + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install UV + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --extra dev + + - name: Check Ruff linting + id: ruff-lint + run: | + echo "Running Ruff linting..." + if ! uv run ruff check .; then + echo "โŒ Linting issues found!" + echo "Run 'uv run ruff check --fix .' to fix auto-fixable issues." + exit 1 + else + echo "โœ… No linting issues found!" + fi + + - name: Check Ruff formatting + id: ruff-format + run: | + echo "Checking Ruff formatting..." + if ! uv run ruff format --check .; then + echo "โŒ Format issues found!" + echo "Run 'uv run ruff format .' to fix formatting issues." + exit 1 + else + echo "โœ… Code format is correct!" + fi + + - name: Success + if: success() + run: | + echo "๐ŸŽ‰ All code quality checks passed!" + echo "Your code meets the quality standards." \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e753460 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,159 @@ +name: Release Package + +on: + workflow_dispatch: + inputs: + version_type: + description: 'Version bump type' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + custom_version: + description: 'Custom version (optional, overrides version_type)' + required: false + type: string + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install UV + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --extra dev + + - name: Configure Git + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + - name: Get current version + id: current_version + run: | + CURRENT_VERSION=$(grep -E '^version = ' pyproject.toml | sed 's/version = "//' | sed 's/"//') + echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "Current version: $CURRENT_VERSION" + + - name: Calculate new version + id: new_version + run: | + CURRENT_VERSION="${{ steps.current_version.outputs.current }}" + + if [ -n "${{ github.event.inputs.custom_version }}" ]; then + NEW_VERSION="${{ github.event.inputs.custom_version }}" + echo "Using custom version: $NEW_VERSION" + elif [ "${{ github.event.inputs.version_type }}" = "major" ]; then + NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{print $1+1".0.0"}') + elif [ "${{ github.event.inputs.version_type }}" = "minor" ]; then + NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{print $1"."$2+1".0"}') + else + # patch (default) + NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{print $1"."$2"."$3+1}') + fi + + echo "new=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "New version: $NEW_VERSION" + + - name: Update version in pyproject.toml + if: github.event_name == 'workflow_dispatch' + run: | + NEW_VERSION="${{ steps.new_version.outputs.new }}" + sed -i "s/version = \".*\"/version = \"$NEW_VERSION\"/" pyproject.toml + echo "Updated pyproject.toml with version $NEW_VERSION" + + - name: Run pre-commit checks + run: | + echo "Running code quality checks..." + uv run ruff check . + uv run ruff format --check . + + - name: Build package + run: | + echo "Building package..." + uv build + + - name: Check package + run: | + echo "Checking built package..." + ls -la dist/ + uv run python -m twine check dist/* + + - name: Commit version bump + if: github.event_name == 'workflow_dispatch' + run: | + NEW_VERSION="${{ steps.new_version.outputs.new }}" + git add pyproject.toml uv.lock + git commit -m "Bump version to $NEW_VERSION" || echo "No changes to commit" + git tag "v$NEW_VERSION" + git push origin HEAD + git push origin "v$NEW_VERSION" + + - name: Publish to PyPI + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + echo "Publishing to PyPI..." + uv run python -m twine upload dist/* + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.event_name == 'workflow_dispatch' && format('v{0}', steps.new_version.outputs.new) || github.ref_name }} + release_name: Release ${{ github.event_name == 'workflow_dispatch' && steps.new_version.outputs.new || github.ref_name }} + body: | + ## Changes in this Release + + - Package version: ${{ github.event_name == 'workflow_dispatch' && steps.new_version.outputs.new || github.ref_name }} + - Built and published to PyPI + + ## Installation + + ```bash + pip install expert==${{ github.event_name == 'workflow_dispatch' && steps.new_version.outputs.new || github.ref_name }} + ``` + + ## Artifacts + + The package has been published to PyPI and is available for installation. + draft: false + prerelease: false + + - name: Upload Release Assets + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: dist/ + asset_name: expert-dist + asset_content_type: application/zip \ No newline at end of file diff --git a/README.md b/README.md index 10e33cd..2e66d3c 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,301 @@ -# Invopop Expert ๐Ÿค– +# Invopop Expert -An AI agent that helps you with Invopop and GOBL (Go Business Language) documentation and implementation questions. It has access to invopop, gobl docs and the [gobl repo](https://github.com/invopop/gobl) +An AI-powered agent library for answering questions about Invopop and GOBL documentation using LangChain and MCP (Model Context Protocol) servers. -## ๐Ÿš€ Quick Start +## Features + +- ๐Ÿค– **Intelligent Q&A**: Answers questions about Invopop and GOBL using advanced RAG +- ๐Ÿ” **Multi-source Search**: Searches through documentation and code repositories +- ๐Ÿ’ฌ **Interactive CLI**: Command-line interface for direct interaction +- ๐Ÿ”ง **Extensible**: Easy to integrate into your own applications +- ๐Ÿ“š **Context Aware**: Maintains conversation history and context + +## Quick Start ### Prerequisites -- Node.js (for MCP servers) +- Python 3.13+ +- Node.js 20+ (for MCP servers) - OpenAI API key -- uv package manager (you can install it with pip or brew: [install uv](https://docs.astral.sh/uv/getting-started/installation/)) ### Installation -1. **Clone the repository:** +> **Note**: This project uses `uv` for dependency management. If you don't have `uv` installed, you can install it with `pip install uv` or use `pipx` for isolated installation. + +1. **Install MCP servers** (required for documentation search): + ```bash + npx mint-mcp add invopop + npx mint-mcp add gobl + ``` + +2. **Install the package**: + ```bash + # Option 1: Development installation (recommended) + git clone https://github.com/invopop/expert.git + cd expert + uv pip install -e . + + # Option 2: Using pipx (installs in isolated environment) + pipx install git+https://github.com/invopop/expert.git + + # Option 3: Direct installation with pip + pip install git+https://github.com/invopop/expert.git + ``` + +3. **Set up environment variables**: + ```bash + export OPENAI_API_KEY=your_openai_api_key + ``` + +4. **Run the CLI**: + ```bash + # If using development installation (Option 1) + uv run expert + + # If using pipx installation (Option 2) + expert + + # If using pip installation (Option 3) + expert + + # Alternative: Run directly without activation (works for all options) + python -m expert.main + ``` + +## CLI Usage + +The CLI provides an interactive chat interface: ```bash -git clone https://github.com/invopop/expert.git -cd expert -``` +$ expert +Welcome to Invopop Expert! Ask questions about GOBL, Invopop and the invopop/gobl library +Enter your multi-line question. Press Enter on an empty line to send. +---------------------------------------------------------------------- -2. **Install MCP servers**: +๐Ÿ‘ค You: How do I create an invoice with GOBL? -```bash -chmod +x scripts/install_mcp_servers.sh -./scripts/install_mcp_servers.sh -``` - -3. **Install Python dependencies**: +๐Ÿค– Thinking... +๐Ÿ” Searching GOBL docs: {"query": "create invoice GOBL"} -```bash -uv sync +๐Ÿค– Assistant: To create an invoice with GOBL, you need to... ``` -4. **Configure environment**: +### CLI Options ```bash -cp .env.example .env -# Edit .env and add your OPENAI_API_KEY +expert --help # Show help +expert --config config.yaml # Use custom config file +expert --verbose # Enable verbose output ``` -5. **Run the agent**: -```bash -uv run python -m src.expert.main +## Library Usage + +You can also use Invopop Expert as a library in your own applications: + +```python +import asyncio +from expert import InvopopExpert, Config + +async def main(): + # Initialize the expert + config = Config() + expert = InvopopExpert(config) + await expert.setup() + + # Ask a question + thread_config = {"configurable": {"thread_id": "my-conversation"}} + response = await expert.get_response( + "How do I handle tax calculations in GOBL?", + thread_config + ) + print(response) + +# Run the example +asyncio.run(main()) ``` -If you have Python > 3.13, you can also run it in cli from root like this: - -Install the package: -```bash -uv pip install -e . +## Configuration + +The agent uses a YAML configuration file (`config.yaml`): + +```yaml +# LLM Configuration +llm: + provider: "openai" + model: "gpt-4.1-2025-04-14" + temperature: 0.1 + +# MCP Server Configuration +mcp: + servers: + invopop: + command: "node" + args: ["~/.mcp/invopop/src/index.js"] + transport: "stdio" + gobl: + command: "node" + args: ["~/.mcp/gobl/src/index.js"] + transport: "stdio" + +# Chat Interface Configuration +chat: + welcome_message: "Welcome to Invopop Expert!" + input_prompt: "Enter your question:" + max_history: 50 ``` -Run it from root: -```bash -uv run expert -``` +### Environment Variables -## ๐Ÿ› ๏ธ Configuration -### Environment Variables (.env) +- `OPENAI_API_KEY` (required): Your OpenAI API key +- `INVOPOP_MCP_PATH` (optional): Custom path to Invopop MCP server +- `GOBL_MCP_PATH` (optional): Custom path to GOBL MCP server -- `OPENAI_API_KEY`: Your OpenAI API key (required) -- `INVOPOP_MCP_PATH`: Custom path to Invopop MCP server (optional) -- `GOBL_MCP_PATH`: Custom path to GOBL MCP server (optional) +## Integration Examples -### Configuration File (config.yaml) -Customize the agent behavior by editing `config.yaml`: +### Building a Web API -- LLM model and parameters -- MCP server paths -- Chat interface settings +```python +from fastapi import FastAPI +from expert import InvopopExpert, Config -## ๐Ÿ’ฌ Usage -Start a conversation with the agent: +app = FastAPI() +expert = InvopopExpert(Config()) -```bash -uv run expert +@app.on_event("startup") +async def startup(): + await expert.setup() + +@app.post("/ask") +async def ask_question(question: str): + thread_config = {"configurable": {"thread_id": "api-user"}} + response = await expert.get_response(question, thread_config) + return {"answer": response} ``` -Ask questions like: +## Architecture -- "How do I create a GOBL invoice?" -- "What are the tax requirements for Spain in Invopop?" -- "How do I integrate with the Invopop API?" +- **LangChain + LangGraph**: AI agent framework with memory and tools +- **MCP Protocol**: Connects to Mintlify documentation servers +- **Multi-source RAG**: Searches Invopop docs, GOBL docs, and code repositories +- **Conversation Memory**: Maintains context across interactions +- **Modular Design**: Easy to extend with new tools and integrations -Or more complex ones like: -- "Give me an example of a valid invoice in Verifactu?" -- "For my case X in greece, what would be the fields required in the invoice and where should they appear?" +## File Structure -In case the result of some prompt is not as expected, report it as an issue and we will look into it. +``` +src/expert/ +โ”œโ”€โ”€ agent.py # Core InvopopExpert agent implementation +โ”œโ”€โ”€ config.py # Configuration management +โ”œโ”€โ”€ main.py # CLI interface +โ”œโ”€โ”€ __init__.py # Package exports +โ””โ”€โ”€ prompts/ # System prompts and tool descriptions + โ”œโ”€โ”€ system_prompt.md + โ”œโ”€โ”€ invopop_docs_description.md + โ”œโ”€โ”€ gobl_docs_description.md + โ””โ”€โ”€ gobl_code_description.md +``` -Type `exit`, `quit`, or `bye` to end the session. +## Development -Type `clear` to start a new thread. +### Local Development Setup -## ๐Ÿงช Development +1. **Clone the repository**: + ```bash + git clone https://github.com/invopop/expert.git + cd expert + ``` + +2. **Install MCP servers**: + ```bash + npx mint-mcp add invopop + npx mint-mcp add gobl + ``` + +3. **Install dependencies**: + ```bash + # Install uv if not already installed + pip install uv + + # Install the package in development mode + uv pip install -e . + ``` + +4. **Set environment variables**: + ```bash + export OPENAI_API_KEY=your_api_key + ``` + +5. **Run the CLI**: + ```bash + # Activate the virtual environment created by uv + source .venv/bin/activate + + # Run the CLI + expert + + # Or run directly without activating the venv + python -m expert.main + ``` + +### Running Tests -### Setup Development Environment ```bash -uv sync --group dev +# Run tests (when implemented) +pytest + +# Run linting +ruff check src/ ``` -### Code Formatting +## Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Add tests for new functionality +5. Ensure all tests pass and linting is clean +6. Commit your changes (`git commit -m 'Add amazing feature'`) +7. Push to the branch (`git push origin feature/amazing-feature`) +8. Open a Pull Request + +## Troubleshooting + +### Common Issues + +**Command 'expert' not found:** ```bash -uv run black src/ -uv run ruff check src/ -``` +# If using development installation, activate the virtual environment +source .venv/bin/activate +expert -## ๐Ÿ“ Project Structure +# Or run directly as a Python module +python -m expert.main + +# If using pipx, ensure pipx bin directory is in your PATH +export PATH="$HOME/.local/bin:$PATH" +``` -``` bash -invopop-expert/ -โ”œโ”€โ”€ src/invopop_expert/ # Main package -โ”œโ”€โ”€ config.yaml # Configuration -โ””โ”€โ”€ scripts/ # Installation scripts +**MCP servers not found:** +```bash +npx mint-mcp add invopop +npx mint-mcp add gobl ``` -## ๐Ÿค Contributing +**OpenAI API errors:** +- Verify your API key is correct and has sufficient credits +- Check that you're using a supported model -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Run tests and formatting -5. Submit a pull request +**Import errors:** +- Ensure you've installed the package: `uv pip install -e .` or `pip install -e .` +- Check that all dependencies are installed +- Try reinstalling: `uv pip install -e . --force-reinstall` + +### Getting Help -## ๐Ÿ“„ License -Apache License 2.0 - see LICENSE file for details. +- ๐Ÿ“– [Invopop Documentation](https://docs.invopop.com) +- ๐Ÿ“š [GOBL Documentation](https://docs.gobl.org/introduction) +- ๐Ÿ› [Report Issues](https://github.com/invopop/expert/issues) -## ๐Ÿ†˜ Support +## License -๐Ÿ“š [Invopop Documentation](https://docs.invopop.com/home) -๐Ÿ“š [GOBL Documentation](https://docs.gobl.org/introduction) -๐Ÿ› [Report Issues or unexpected answer](https://github.com/invopop/expert/issues) \ No newline at end of file +See [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7ff1d15..8558ce1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "expert" version = "0.1.0" -description = "Add your description here" +description = "Invopop Expert - AI agent library for Invopop and GOBL documentation" readme = "README.md" requires-python = ">=3.13" dependencies = [ @@ -13,13 +13,46 @@ dependencies = [ "langgraph-prebuilt>=0.2.2", ] +[project.optional-dependencies] +dev = [ + "ruff>=0.6.0", + "twine>=5.0.0", +] + [project.scripts] expert = "expert.main:cli" +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-dir] +"" = "src" + +[tool.setuptools.package-data] +expert = ["prompts/*.md"] + [tool.ruff] line-length = 88 target-version = "py313" -[tool.black] -line-length = 88 -target-version = ['py313'] +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" diff --git a/src/expert/__init__.py b/src/expert/__init__.py index e69de29..184cda0 100644 --- a/src/expert/__init__.py +++ b/src/expert/__init__.py @@ -0,0 +1,31 @@ +""" +Invopop Expert - AI agent library for Invopop and GOBL documentation. + +This package provides an AI-powered agent that can answer questions about +Invopop and GOBL using advanced retrieval-augmented generation (RAG) with +MCP (Model Context Protocol) servers. + +Example usage: + import asyncio + from expert import InvopopExpert, Config + + async def main(): + config = Config() + expert = InvopopExpert(config) + await expert.setup() + + thread_config = {"configurable": {"thread_id": "my-conversation"}} + response = await expert.get_response( + "How do I create an invoice?", + thread_config + ) + print(response) + + asyncio.run(main()) +""" + +from .agent import InvopopExpert +from .config import Config + +__version__ = "0.1.0" +__all__ = ["InvopopExpert", "Config"] diff --git a/src/expert/agent.py b/src/expert/agent.py index 2cb6375..d40e752 100644 --- a/src/expert/agent.py +++ b/src/expert/agent.py @@ -2,12 +2,12 @@ from datetime import datetime from pathlib import Path -from typing import Dict, Any +from typing import Any -from langchain_mcp_adapters.client import MultiServerMCPClient -from langgraph.prebuilt import create_react_agent from langchain_core.tools import StructuredTool +from langchain_mcp_adapters.client import MultiServerMCPClient from langgraph.checkpoint.memory import InMemorySaver +from langgraph.prebuilt import create_react_agent from .config import Config @@ -26,16 +26,16 @@ def _load_prompts(self): """Load prompt templates from files.""" prompts_dir = Path(__file__).parent / "prompts" - with open(prompts_dir / "system_prompt.md", "r") as f: + with open(prompts_dir / "system_prompt.md") as f: self.system_prompt = f.read().strip() - with open(prompts_dir / "invopop_docs_description.md", "r") as f: + with open(prompts_dir / "invopop_docs_description.md") as f: self.invopop_docs_description = f.read().strip() - with open(prompts_dir / "gobl_docs_description.md", "r") as f: + with open(prompts_dir / "gobl_docs_description.md") as f: self.gobl_docs_description = f.read().strip() - with open(prompts_dir / "gobl_code_description.md", "r") as f: + with open(prompts_dir / "gobl_code_description.md") as f: self.gobl_code_description = f.read().strip() async def setup(self): @@ -60,8 +60,12 @@ async def setup(self): new_name = "gobl_code_ask_question" new_description = self.gobl_code_description new_schema = tool.args_schema.copy() - new_schema['properties']['repoName']['description'] = "This value will always be 'invopop/gobl'" - new_schema['properties']['question']['description'] = "The question to ask about the invopop/gobl repo" + new_schema["properties"]["repoName"]["description"] = ( + "This value will always be 'invopop/gobl'" + ) + new_schema["properties"]["question"]["description"] = ( + "The question to ask about the invopop/gobl repo" + ) else: continue # Create a new StructuredTool with the new name @@ -76,7 +80,9 @@ async def setup(self): # Create the agent llm_config = self.config.llm_config - model_name = f"{llm_config.get('provider', 'openai')}:{llm_config.get('model', 'gpt-4.1')}" + provider = llm_config.get("provider", "openai") + model = llm_config.get("model", "gpt-4.1") + model_name = f"{provider}:{model}" self.agent = create_react_agent( model_name, @@ -85,7 +91,7 @@ async def setup(self): prompt=self.system_prompt, ) - async def get_response(self, user_input: str, config: Dict[str, Any]) -> str: + async def get_response(self, user_input: str, config: dict[str, Any]) -> str: """Get response from the agent for a given input.""" if not self.agent: raise RuntimeError("Agent not initialized. Call setup() first.") @@ -106,8 +112,8 @@ async def get_response(self, user_input: str, config: Dict[str, Any]) -> str: elif func_name == "gobl_search": print( "๐Ÿ” Searching GOBL docs:", - tool_call["function"]["arguments"], - ) + tool_call["function"]["arguments"], + ) elif func_name == "gobl_code_ask_question": print( "๐Ÿ” Searching invopop/gobl repo:", diff --git a/src/expert/config.py b/src/expert/config.py index ed3c9a5..d672449 100644 --- a/src/expert/config.py +++ b/src/expert/config.py @@ -1,16 +1,17 @@ """Configuration management for Invopop Expert.""" import os -import yaml from pathlib import Path -from typing import Dict, Any, Optional +from typing import Any + +import yaml from dotenv import load_dotenv class Config: """Configuration manager for Invopop Expert.""" - def __init__(self, config_path: Optional[str] = None): + def __init__(self, config_path: str | None = None): """Initialize configuration.""" # Load environment variables load_dotenv() @@ -25,15 +26,17 @@ def __init__(self, config_path: Optional[str] = None): # Validate required environment variables self._validate_env_vars() - def _load_config(self) -> Dict[str, Any]: + def _load_config(self) -> dict[str, Any]: """Load configuration from YAML file.""" try: - with open(self.config_path, "r") as f: + with open(self.config_path) as f: return yaml.safe_load(f) - except FileNotFoundError: - raise FileNotFoundError(f"Configuration file not found: {self.config_path}") + except FileNotFoundError as e: + raise FileNotFoundError( + f"Configuration file not found: {self.config_path}" + ) from e except yaml.YAMLError as e: - raise ValueError(f"Invalid YAML configuration: {e}") + raise ValueError(f"Invalid YAML configuration: {e}") from e def _validate_env_vars(self): """Validate required environment variables.""" @@ -46,12 +49,12 @@ def openai_api_key(self) -> str: return os.getenv("OPENAI_API_KEY") @property - def llm_config(self) -> Dict[str, Any]: + def llm_config(self) -> dict[str, Any]: """Get LLM configuration.""" return self.config.get("llm", {}) @property - def mcp_config(self) -> Dict[str, Any]: + def mcp_config(self) -> dict[str, Any]: """Get MCP server configuration.""" config = self.config.get("mcp", {}).get("servers", {}) @@ -74,6 +77,6 @@ def mcp_config(self) -> Dict[str, Any]: return config @property - def chat_config(self) -> Dict[str, Any]: + def chat_config(self) -> dict[str, Any]: """Get chat configuration.""" return self.config.get("chat", {}) diff --git a/src/expert/main.py b/src/expert/main.py index 42a66b7..c651124 100644 --- a/src/expert/main.py +++ b/src/expert/main.py @@ -5,8 +5,8 @@ import click -from .config import Config from .agent import InvopopExpert +from .config import Config @click.command() diff --git a/uv.lock b/uv.lock index 8ecb379..5975c3c 100644 --- a/uv.lock +++ b/uv.lock @@ -98,6 +98,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "cryptography" +version = "45.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335 }, + { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487 }, + { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922 }, + { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433 }, + { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163 }, + { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687 }, + { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623 }, + { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447 }, + { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830 }, + { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746 }, + { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456 }, + { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495 }, + { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540 }, + { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052 }, + { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024 }, + { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442 }, + { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038 }, + { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964 }, +] + [[package]] name = "distro" version = "1.9.0" @@ -107,6 +136,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, ] +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + [[package]] name = "dotenv" version = "0.9.9" @@ -121,7 +159,7 @@ wheels = [ [[package]] name = "expert" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "dotenv" }, { name = "langchain" }, @@ -131,6 +169,12 @@ dependencies = [ { name = "langgraph-prebuilt" }, ] +[package.optional-dependencies] +dev = [ + { name = "ruff" }, + { name = "twine" }, +] + [package.metadata] requires-dist = [ { name = "dotenv", specifier = ">=0.9.9" }, @@ -139,7 +183,10 @@ requires-dist = [ { name = "langchain-openai", specifier = ">=0.3.18" }, { name = "langgraph", specifier = ">=0.4.7" }, { name = "langgraph-prebuilt", specifier = ">=0.2.2" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.0" }, + { name = "twine", marker = "extra == 'dev'", specifier = ">=5.0.0" }, ] +provides-extras = ["dev"] [[package]] name = "greenlet" @@ -212,6 +259,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611 }, +] + [[package]] name = "idna" version = "3.10" @@ -221,6 +280,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1c/831faaaa0f090b711c355c6d8b2abf277c72133aab472b6932b03322294c/jaraco_functools-4.2.1.tar.gz", hash = "sha256:be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353", size = 19661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/fd/179a20f832824514df39a90bb0e5372b314fea99f217f5ab942b10a8a4e8/jaraco_functools-4.2.1-py3-none-any.whl", hash = "sha256:590486285803805f4b1f99c60ca9e94ed348d4added84b74c7a12885561e524e", size = 10349 }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, +] + [[package]] name = "jiter" version = "0.10.0" @@ -278,6 +379,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 }, ] +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085 }, +] + [[package]] name = "langchain" version = "0.3.25" @@ -427,6 +545,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/8e/e8a58e0abaae3f3ac4702e9ca35d1fc6159711556b64ffd0e247771a3f12/langsmith-0.3.42-py3-none-any.whl", hash = "sha256:18114327f3364385dae4026ebfd57d1c1cb46d8f80931098f0f10abe533475ff", size = 360334 }, ] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + [[package]] name = "mcp" version = "1.9.1" @@ -447,6 +577,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/c0/4ac795585a22a0a2d09cd2b1187b0252d2afcdebd01e10a68bbac4d34890/mcp-1.9.1-py3-none-any.whl", hash = "sha256:2900ded8ffafc3c8a7bfcfe8bc5204037e988e753ec398f371663e6a06ecd9a9", size = 130261 }, ] +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278 }, +] + +[[package]] +name = "nh3" +version = "0.2.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678 }, + { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774 }, + { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012 }, + { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619 }, + { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384 }, + { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908 }, + { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180 }, + { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747 }, + { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908 }, + { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133 }, + { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328 }, + { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020 }, + { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878 }, + { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460 }, + { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369 }, + { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036 }, + { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712 }, + { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559 }, + { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591 }, + { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670 }, + { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093 }, + { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623 }, + { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283 }, +] + [[package]] name = "openai" version = "1.82.0" @@ -580,6 +759,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, ] +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + [[package]] name = "python-dotenv" version = "1.1.0" @@ -598,6 +786,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -615,6 +812,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310 }, +] + [[package]] name = "regex" version = "2024.11.6" @@ -665,6 +876,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, ] +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, +] + +[[package]] +name = "ruff" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516 }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083 }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024 }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324 }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416 }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197 }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615 }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080 }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315 }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640 }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364 }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462 }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028 }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992 }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944 }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669 }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928 }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -759,6 +1030,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, ] +[[package]] +name = "twine" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791 }, +] + [[package]] name = "typing-extensions" version = "4.13.2"