diff --git a/.github/workflows/build-and-push-docker-image.yaml b/.github/workflows/build-and-push-docker-image.yaml index cc8b185..afff89e 100644 --- a/.github/workflows/build-and-push-docker-image.yaml +++ b/.github/workflows/build-and-push-docker-image.yaml @@ -1,43 +1,93 @@ -name: Crews-Control Docker Image +name: Build and Push Crews-Control Docker Image on: workflow_dispatch: + push: + branches: + - 'main' jobs: - crews-control-docker-image: - runs-on: [self-hosted-common-strong] - environment: crews-control + build-and-push: + runs-on: ubuntu-latest + environment: crews-control + permissions: + contents: read + packages: write + steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Free up disk space on the runner + uses: jlumbroso/free-disk-space@main with: - path: 'crews-control-action' - ref: 'main' + # This is the key. We don't touch the tool-cache where Python lives. + tool-cache: false + # These are large directories that are safe to remove for this project. + android: true + dotnet: true + haskell: true + # See the action's documentation for other options - - name: Get the commit SHA of Crews-Control - run: echo "CREWS_CONTROL_SHA=$(git -C crews-control-action rev-parse HEAD)" >> $GITHUB_ENV + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12.3' - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # aws-actions/configure-aws-credentials@v1 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable with: - aws-access-key-id: ${{ secrets.ARTIFACTS_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.ARTIFACTS_AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.ECR_AWS_REGION }} - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@261a7de32bda11ba01f4d75c4ed6caf3739e54be # aws-actions/amazon-ecr-login@v1 - - - name: Build, tag and push the image to ECR - id: build-push - working-directory: 'crews-control-action' - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: ${{ secrets.CREWS_CONTROL_ACTION_IMAGE_NAME }} + toolchain: stable + + - name: Compile requirements with a stable and compatible toolset run: | - echo "Building the image" + # Pin all core packaging tools to a known-good, compatible set + python -m pip install --upgrade "pip==24.0" "setuptools" "wheel" "pip-tools==7.4.1" + + echo "---" + echo "DEBUG: Checking installed tool versions" + pip --version + pip-compile --version + echo "---" + + # Run the compilation, which will now use a stable environment + echo "Compiling requirements.in..." + pip-compile --generate-hashes --verbose --no-strip-extras requirements.in - docker build --no-cache \ - -t "$ECR_REGISTRY/$ECR_REPOSITORY:${CREWS_CONTROL_SHA}" \ - . - echo "Pushing image to ECR" - docker push "$ECR_REGISTRY/$ECR_REPOSITORY:${CREWS_CONTROL_SHA}" + echo "Compiling requirements-dev.in..." + pip-compile --generate-hashes --verbose --no-strip-extras requirements-dev.in + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository }} + tags: | + type=sha,prefix= + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/run-crews-control-project.yaml b/.github/workflows/run-crews-control-project.yaml new file mode 100644 index 0000000..fc22a4c --- /dev/null +++ b/.github/workflows/run-crews-control-project.yaml @@ -0,0 +1,122 @@ +name: Run Crews-Control Project + +on: + workflow_dispatch: + inputs: + project_name: + description: 'The name of the project to run (e.g., pr-security-review).' + required: true + project_source: + description: 'Source of the project files (execution.yaml, context/, etc.).' + type: choice + options: + - repository # Use files from this Git repo (for testing changes) + - image # Use files already inside the Docker image + default: 'repository' + image_tag: + description: 'The Docker image tag to pull from Docker Hub.' + required: true + default: 'latest' + docker_image: + description: 'The full name of the Docker image.' + required: true + default: 'ghcr.io/avri-schneider/crews-control' + run_params: + description: 'Optional: Command-line parameters for the run (e.g., key1=value1 key2="value 2").' + required: false + +jobs: + run-crews-control-project: + runs-on: ubuntu-latest + environment: crews-control + permissions: + contents: read + packages: read + + steps: + - name: Checkout repository files (if running from repository) + if: ${{ inputs.project_source == 'repository' }} + uses: actions/checkout@v4 + + - name: Create .env file from secrets + run: | + # This step securely creates the .env file needed by the Docker container + echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> .env + echo "JIRA_API_TOKEN=${{ secrets.JIRA_API_TOKEN }}" >> .env + echo "JIRA_USERNAME=${{ vars.JIRA_USERNAME }}" >> .env + echo "JIRA_INSTANCE_URL=${{ vars.JIRA_INSTANCE_URL }}" >> .env + echo "JIRA_CREATE_ISSUE_PROJECT_KEY=${{ vars.JIRA_CREATE_ISSUE_PROJECT_KEY }}" >> .env + echo "JIRA_CREATE_ISSUE_TYPE=${{ vars.JIRA_CREATE_ISSUE_TYPE }}" >> .env + echo "JIRA_LINK_ALLOWED_PAIRS=${{ vars.JIRA_LINK_ALLOWED_PAIRS }}" >> .env + echo "JIRA_ATTACH_ALLOWED_PREFIXES=${{ vars.JIRA_ATTACH_ALLOWED_PREFIXES }}" >> .env + echo "JIRA_REASSIGN_ALLOWED_PREFIXES=${{ vars.JIRA_REASSIGN_ALLOWED_PREFIXES }}" >> .env + echo "JIRA_SETPRIORITY_ALLOWED_PREFIXES=${{ vars.JIRA_SETPRIORITY_ALLOWED_PREFIXES }}" >> .env + echo "AZURE_API_KEY=${{ vars.AZURE_API_KEY }}" >> .env + echo "AZURE_API_BASE=${{ vars.AZURE_API_BASE }}" >> .env + echo "AZURE_API_VERSION=${{ vars.AZURE_API_VERSION }}" >> .env + echo "AZURE_OPENAI_VISION_DEPLOYMENT=${{ vars.AZURE_OPENAI_VISION_DEPLOYMENT }}" >> .env + echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env + echo "OPENAI_API_VERSION=${{ vars.OPENAI_API_VERSION }}" >> .env + echo "OPENAI_MODEL_NAME=${{ vars.OPENAI_MODEL_NAME }}" >> .env + echo "OPENAI_EMBEDDING_MODEL_NAME=${{ vars.OPENAI_EMBEDDING_MODEL_NAME }}" >> .env + echo "OPENAI_VISION_MODEL=${{ vars.OPENAI_VISION_MODEL }}" >> .env + echo "LLM_NAME=${{ vars.LLM_NAME }}" >> .env + echo "EMBEDDER_NAME=${{ vars.EMBEDDER_NAME }}" >> .env + echo "CONFLUENCE_ENDPOINT=${{ vars.CONFLUENCE_ENDPOINT }}" >> .env + echo "CONFLUENCE_API_USER=${{ vars.CONFLUENCE_API_USER }}" >> .env + echo "CONFLUENCE_API_TOKEN=${{ secrets.CONFLUENCE_API_TOKEN }}" >> .env + echo "CONFLUENCE_SPACE=${{ vars.CONFLUENCE_SPACE }}" >> .env + # Add any other secrets from your .env.example here + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # - name: Log in to Docker Hub + # uses: docker/login-action@v3 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Pull Docker image + run: docker pull ${{ inputs.docker_image }}:${{ inputs.image_tag }} + + - name: Run Project from Repository Source + if: ${{ inputs.project_source == 'repository' }} + run: | + docker run --rm \ + -e HOST_USER_ID=$(id -u) \ + -e HOST_GROUP_ID=$(id -g) \ + --env-file .env \ + -v ${{ github.workspace }}/projects/${{ inputs.project_name }}:/app/projects/${{ inputs.project_name }} \ + ${{ inputs.docker_image }}:${{ inputs.image_tag }} \ + --project-name ${{ inputs.project_name }} \ + --params ${{ inputs.run_params }} + + - name: Run Project from Image Source + if: ${{ inputs.project_source == 'image' }} + run: | + # Create a directory on the host to capture the output artifacts + mkdir -p ${{ github.workspace }}/outputs/${{ inputs.project_name }} + + docker run --rm \ + -e HOST_USER_ID=$(id -u) \ + -e HOST_GROUP_ID=$(id -g) \ + --env-file .env \ + -v ${{ github.workspace }}/outputs/${{ inputs.project_name }}:/app/projects/${{ inputs.project_name }}/output \ + ${{ inputs.docker_image }}:${{ inputs.image_tag }} \ + --project-name ${{ inputs.project_name }} \ + --params ${{ inputs.run_params }} + + - name: Upload Project Artifacts + if: always() # Always run this step to capture logs and outputs even if the run fails + uses: actions/upload-artifact@v4 + with: + name: output-${{ inputs.project_name }}-${{ github.run_id }} + path: | + ${{ github.workspace }}/projects/${{ inputs.project_name }}/output/ + ${{ github.workspace }}/outputs/${{ inputs.project_name }}/ + if-no-files-found: ignore diff --git a/Dockerfile b/Dockerfile index 492d8ca..8c45a78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,65 +1,42 @@ -# Stage 1: Build and compile everything in a full Python image +# Stage 1: Build stage for installing dependencies FROM python:3.12.3 AS build -# Set the working directory in the container WORKDIR /app -# Copy only the requirements file, to cache the installed packages layer COPY requirements.txt /app/ -# Install system dependencies for building packages (if any needed) -RUN apt-get update && apt-get install -y \ - build-essential \ - libssl-dev \ - libffi-dev \ - python3-dev \ - && rm -rf /var/lib/apt/lists/* - -# Upgrade pip and install dependencies with retries RUN pip install --upgrade pip setuptools \ - && pip install --require-hashes --no-cache-dir -r requirements.txt --verbose -# Suggested retry mechanism for pip install (commented out) -# RUN pip install --upgrade pip setuptools && \ -# pip install --require-hashes --no-cache-dir -r requirements.txt || \ -# pip install --require-hashes --no-cache-dir -r requirements.txt - - -# Invalidate cache from here onwards when needed -ARG CACHEBUSTER=1 + && pip install --require-hashes --no-cache-dir -r requirements.txt --verbose \ + && rm -rf /root/.cache/pip -# Create a non-root user 'appuser' and switch to it -RUN groupadd appuser && \ - useradd -m -g appuser appuser - -USER appuser - -# Copy the current directory contents into the container at /app COPY . /app -# Stage 2: Create a slim image for running the application + +# Stage 2: Final slim image for production FROM python:3.12.3-slim -# Copy user and group data -COPY --from=build /etc/passwd /etc/passwd -COPY --from=build /etc/group /etc/group +# Install gosu, a lightweight tool for switching users, then clean up. +RUN apt-get update && apt-get install -y --no-install-recommends gosu \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* -# Copy installed Python packages from build stage -COPY --from=build /usr/local/lib/python3.12 /usr/local/lib/python3.12 +WORKDIR /app -# Ensure scripts in /usr/local/bin are available +# Copy installed dependencies and application code +COPY --from=build /usr/local/lib/python3.12 /usr/local/lib/python3.12 COPY --from=build /usr/local/bin /usr/local/bin - -# Copy application code and other necessary files from build stage COPY --from=build /app /app -# Ensure the appuser owns the necessary directories -RUN mkdir -p /home/appuser && \ - chown -R appuser:appuser /home/appuser && \ +# Create a generic appuser with a standard home directory +RUN useradd --create-home --shell /bin/bash appuser && \ chown -R appuser:appuser /app -# Set the working directory and user -WORKDIR /app -USER appuser +# Copy the entrypoint script and make it executable +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Combine the entrypoint script and the main command. +ENTRYPOINT ["entrypoint.sh", "python", "-u", "crews_control.py"] -ENTRYPOINT [ "python", "-u", "crews_control.py" ] -CMD [] \ No newline at end of file +# The default command is now empty, as the main command is in the ENTRYPOINT. +CMD [] diff --git a/README.md b/README.md index 7805157..a02d33f 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,36 @@ This project builds upon the following MIT-licensed project: **Crews Control** is an abstraction layer on top of [crewAI](https://www.crewai.com/), designed to facilitate the creation and execution of AI-driven projects without writing code. By defining an `execution.yaml` file, users can orchestrate AI crews to accomplish complex tasks using predefined or custom tools. +## Core Concepts & Architecture + +Crews Control orchestrates workflows by connecting a few key components defined in your `execution.yaml`. + +* **Orchestrator:** The engine that reads your YAML and manages the overall workflow. +* **Crew:** A group of Agents assigned to complete a set of related Tasks. +* **Agent:** An autonomous AI worker with a specific role, goal, and set of Tools. +* **Task:** A single, well-defined unit of work performed by an Agent. + +The relationship between these components follows this high-level architecture: + +```mermaid +graph TD; + A[execution.yaml] --> B(Crews Control Orchestrator); + B --> C{Crew 1}; + B --> D{Crew 2}; + C --> E[crewAI Agents/Tasks]; + D --> F[crewAI Agents/Tasks]; + E --> G([Output Artifact]); + F --> H([Output Artifact]); +``` + ## Features - - **No-Code AI Orchestration:** Define projects with `execution.yaml`, specifying crews, agents, and tasks. - - **Advanced Conditional Logic:** Orchestrate complex workflows with `and`/`or` dependencies based on crew status (`SUCCESS`, `SKIPPED`) or output content (`output_contains`, `output_not_contains`). - - **Dynamic Task Inputs:** Define task inputs (`description`, `expected_output`, etc.) dynamically at runtime based on the outcomes of previous crews using a `resolved_inputs` block. - - **Modular Tools:** Use a set of predefined tools or create custom ones. - - **Artifact Generation:** Each crew outputs a file artifact from the final task. - - **Templated Outputs:** Access outputs from previous crews’ tasks using a powerful templating syntax. +- **No-Code AI Orchestration:** Define projects with `execution.yaml`, specifying crews, agents, and tasks. +- **[Advanced Conditional Logic & Control Flow](#2-conditional-dependencies-dependson):** Orchestrate complex workflows with `and`/`or` dependencies, `run_until` loops, and `for_each` iterators. +- **[Per-Agent LLM Configuration](#6-per-agent-llms-llm_model):** Assign specific LLM providers and models to individual agents for fine-tuned performance and cost optimization. +- **[Dynamic & External Inputs](#5-external-content-the-context-block):** Define task inputs dynamically based on previous outcomes and load content from external files. +- **Modular Tools:** Use predefined tools or create your own to inject functionality into tasks. +- **[Deterministic Artifact Generation](#4-deterministic-naming-sha256):** Create unique, consistent output filenames based on input content. ## Licensing @@ -214,108 +236,373 @@ Example - run the `pr-security-review` project to review `PR #1` of the `Axonius make run project_name=pr-security-review PARAMS="github_repo_name='Axonius/crews-control' pr_number='1'" ``` -### Creating a Project +## Creating a Project: From Basic to Advanced -1. Create a subfolder `projects/project_name`. -2. Inside the subfolder, create a file named `execution.yaml`. The file can have the following structure: +To get started, every project requires its own folder and a main `execution.yaml` file. -```yaml -settings: - output_results: true +1. **Create a Project Folder:** All projects live inside the `/projects` directory. + ```bash + mkdir projects/my-new-project + ``` +2. **Create an `execution.yaml` File:** Inside your new folder, create the main configuration file. + ```bash + touch projects/my-new-project/execution.yaml + ``` -user_inputs: - user_input_1: - title: "User input 1" - user_input_2: - title: "User input 2" +> **Pro-Tip:** You can use the built-in `bot-generator` project to create a boilerplate `execution.yaml` for you. + +The following sections provide self-contained examples of what you can put inside your `execution.yaml` file, each demonstrating a core feature. + +----- + +### 1\. Basic Dependency +This is the simplest functional project. It defines two crews. `writer_crew` has a `depends_on` block, ensuring it only runs after `research_crew` is finished. The output of the first crew is available as a `{research_crew}` placeholder. + +```yaml +user_inputs: + topic: + title: "Blog Post Topic" crews: - data_gathering_crew: - output_naming_template: 'output_data_gathering_{user_input_1}.md' + research_crew: agents: - data_gatherer_agent: - role: "Data Gatherer" - goal: "Gather initial data based on {user_input_1} and {user_input_2}." + researcher: + role: "Researcher" + goal: "Find 3 key facts about {topic}." tools: [human] - backstory: "An agent that collects initial information." + backstory: "An expert researcher." tasks: - gather_task: - agent: data_gatherer_agent - description: "Collect data based on {user_input_1}." - expected_output: "A summary of the gathered data." + research_task: + agent: researcher + description: "Please provide 3 key facts about {topic}." + expected_output: "A bulleted list." + writer_crew: + depends_on: + - research_crew + agents: + writer: + role: "Writer" + goal: "Write a short paragraph based on the research." + tools: [] + backstory: "A skilled writer." + tasks: + write_task: + agent: writer + description: "Write a paragraph based on these facts:\n{research_crew}" + expected_output: "A single paragraph." +``` +----- + +### 2\. Conditional Dependencies (`depends_on`) + +You can create powerful, branching workflows by adding a `condition` block to any dependency. The `condition` block supports three keys, which are checked with logical **AND** if multiple are used within the same condition. + + * `output_contains`: The crew runs if this string **is found** in the dependency's output. + * `output_not_contains`: The crew runs if this string is **not found** in the dependency's output. + * `status`: The crew runs if the dependency finished with a specific status (e.g., `SUCCESS`, `SKIPPED`). - triage_crew: +You can combine multiple dependencies using `and` or `or` for complex scenarios. In the example below, the `escalation_crew` runs if **either** the manager explicitly says to escalate, **or** if a vulnerability was found **and** the manager was unavailable (i.e., their crew was skipped). + +```yaml +user_inputs: + scan_target: + title: "Scan Target" +crews: + analysis_crew: + agents: + analyzer: + role: "Security Analyzer" + goal: "Analyze the target and report vulnerabilities." + tasks: + analyze_task: + agent: analyzer + description: "Scan {scan_target}. If clean, respond with 'Target is CLEAN'." + + manager_approval_crew: + # This crew only runs if the analysis is NOT clean depends_on: - - data_gathering_crew - output_naming_template: 'output_triage_{user_input_1}.md' + - crew: analysis_crew + condition: + output_not_contains: 'CLEAN' agents: - triage_agent: - role: "Triage Specialist" - goal: "Analyze data and decide if a full analysis is needed." + manager: + role: "Manager" + goal: "Approve security findings for escalation." tools: [human] - backstory: "An agent that makes decisions based on initial data." tasks: - triage_task: - agent: triage_agent - description: "Analyze the output from the data gathering crew: {data_gathering_crew}. If a deep analysis is needed, your final answer must contain the phrase 'FULL_ANALYSIS_REQUIRED'." - expected_output: "A decision string, either containing 'FULL_ANALYSIS_REQUIRED' or not." + approval_task: + agent: manager + description: "Findings for {scan_target}:\n{analysis_crew}\nRespond with 'ESCALATE' or 'IGNORE'." - deep_analysis_crew: - # FEATURE: Advanced conditional dependencies + escalation_crew: depends_on: - and: # This crew runs only if BOTH conditions below are met - - crew: data_gathering_crew # Condition 1: a simple dependency - - crew: triage_crew # Condition 2: a dependency with a specific condition + or: + # Case 1: The manager explicitly approved escalation. + - crew: manager_approval_crew condition: - output_contains: 'FULL_ANALYSIS_REQUIRED' - output_naming_template: 'output_deep_analysis_{user_input_1}.md' + output_contains: 'ESCALATE' + # Case 2: The analysis found something AND the manager was unavailable to review it. + - and: + - crew: analysis_crew + condition: + output_not_contains: 'CLEAN' + - crew: manager_approval_crew + status: 'SKIPPED' agents: - analysis_agent: - role: "Analysis Expert" - goal: "Perform a deep analysis." + escalator: + role: "Escalation Lead" + goal: "Handle the security escalation." + tasks: + escalation_task: + agent: escalator + description: "Handle the security escalation for {scan_target} based on these findings:\n{analysis_crew}" +``` + +----- + +### 3\. Looping with `run_until` + +A crew can be set to run repeatedly until its output meets specific conditions. This `validator_crew` will run up to 5 times until its output contains "SUCCESS" **and** does not contain "ERROR". This is ideal for polling, validation, or self-correction loops. + +```yaml +crews: + validator_crew: + run_until: + max_retries: 5 + delay_seconds: 10 + condition: + output_contains: "SUCCESS" + output_not_contains: "ERROR" + agents: + validator: + role: "Validator" + goal: "Validate a process and report its status." tools: [human] - backstory: "An agent that performs in-depth analysis." tasks: - analysis_task: - agent: analysis_agent - description: "Perform a deep and thorough analysis based on the initial data from {data_gathering_crew}." - expected_output: "A detailed report of the findings." + validation_task: + agent: validator + description: "Please check the system status. If it's fully operational, respond with 'STATUS: SUCCESS'. If there is a problem, respond with 'STATUS: ERROR'." +``` - final_summary_crew: - # depends_on can also be used structurally to ensure execution order - depends_on: - - deep_analysis_crew - - triage_crew - output_naming_template: 'output_final_summary_{user_input_1}.md' +**How it Works:** + + * The crew's output is checked after each run. All conditions in the `condition` block must be satisfied. + * The loop will only stop when the output contains "SUCCESS" **AND** does not contain "ERROR". + * If conditions are not met, it will retry until `max_retries` is reached. + * **Note:** Set `max_retries: -1` for an infinite loop, which is useful for service-like polling. + +#### Special Values + +| Value | Meaning | +| ----------------- | ------------------------------------------ | +| `max_retries: -1` | Infinite retries (loop until success) | +| `max_retries: 1+` | Retry that many times at most | +| `max_retries: 0` | ❌ Invalid — remove `run_until` to run once | +| `< -1` | ❌ Invalid — will raise an error | + +#### Notes + +* `delay_seconds` is optional but useful for rate limits or external dependencies. +* You can omit either `output_contains` or `output_not_contains` if only one condition is needed. +* Cached outputs are always ignored during retries to ensure fresh execution. +----- + +### 4\. Iterating with `for_each` + +The for_each property configures a crew to run its logic for every item in a list, making it ideal for processing a variable number of items like file chunks, search results, or API responses. + +This property takes a string value that must resolve to a valid JSON array. This provides flexibility in how the list of items is generated: it can be hardcoded directly into the workflow, or dynamically generated by a previous crew's output. + +**Example with a Hardcoded List:** +```yaml +crews: + security_review_iterator: + # The list of items is defined directly as a static JSON array string. + for_each: '["User Onboarding", "Admin Dashboard", "Payment Gateway"]' + + agents: + reviewer: + role: "Security Analyst" + goal: "Brainstorm threats for a specific software feature." + + tasks: + brainstorm_task: + agent: reviewer + description: "List 3 potential security threats for the '{item}' feature." + expected_output: "A bulleted list of three threats." +``` + +**Example with a Dynamically Generated List:** +```yaml +user_inputs: + topic: + title: "Topic to research" +crews: + # 1. This crew generates a list of sub-topics + sub_topic_generator_crew: + agents: + planner: + role: "Planner" + goal: "Generate a list of 3 related sub-topics for {topic}." + tasks: + generate_task: + agent: planner + description: "Generate 3 sub-topics related to {topic}." + expected_output: > + A single JSON array string. Example: ["Sub-topic A", "Sub-topic B", "Sub-topic C"] + + # 2. This crew iterates over the list from the previous crew + research_iterator_crew: + depends_on: [sub_topic_generator_crew] + for_each: "{sub_topic_generator_crew}" + + # The rest of the definition is a normal crew agents: - summary_agent: - role: "Summarizer" - goal: "Create a final summary of the entire process." + researcher: + role: "Researcher" + goal: "Find key facts about a sub-topic." + tasks: + research_task: + agent: researcher + description: "Find 3 key facts about this specific sub-topic: {item}" + expected_output: "A bulleted list of 3 facts for the sub-topic." +``` + +#### How it Works: + +- **`for_each: "{...}"`**: This property on a crew triggers the iteration. Its value is a template that must resolve to a JSON array string. +- **`{item}` Placeholder**: This is a special, reserved keyword. For each iteration, the framework replaces `{item}` with the current value from the list. +- **Aggregated Output**: The final output of the iterator crew (e.g., `{research_iterator_crew}`) will be a single JSON array string containing the results from all the individual runs. + +### 5\. Per-Agent LLM Assignment + +Optimize for cost and performance by assigning different LLMs to different agents. In this example, the `researcher` uses a fast, inexpensive model, while the `writer` uses a more powerful, creative model. + +```yaml +user_inputs: + topic: + title: "Topic" +crews: + research_crew: + agents: + researcher: + llm_model: + provider: 'groq' + model_name: 'llama3-8b-8192' + role: "Researcher" + goal: "Find facts about {topic}." tools: [human] - backstory: "An agent that compiles final reports." + backstory: "An expert." + tasks: + research_task: + agent: researcher + description: "Provide facts on {topic}." + writer_crew: + depends_on: [research_crew] + agents: + writer: + llm_model: + provider: 'openai' + model_name: 'gpt-4o' + role: "Writer" + goal: "Write a blog post about {topic}." + tools: [] + backstory: "A creative writer." tasks: - summary_task: - agent: summary_agent - # The description is built dynamically using a resolved input - description: "{summary_introduction} Based on this, create a final, concise summary." - expected_output: "A final, easy-to-read summary document." - # FEATURE: Dynamic input resolution + write_task: + agent: writer + description: "Write a post using these facts:\n{research_crew}" +``` + +----- + +### 6\. Dynamic Task Inputs with `resolved_inputs` + +Dynamically construct parts of a task's description based on the results of previous crews. This `summary_crew` changes its `final_summary` placeholder based on whether the `approval_crew` succeeded or was skipped. + +```yaml +crews: + approval_crew: + run_until: + max_retries: 3 + condition: + output_contains: "APPROVE" + agents: + validator: + role: "Validator" + goal: "Get approval." + tools: [human] + tasks: + validation_task: + agent: validator + description: "Review this. To approve, respond with 'APPROVE'." + expected_output: "The word 'APPROVE'." + summary_crew: + depends_on: [approval_crew] + agents: + reporter: + role: "Reporter" + goal: "Summarize the outcome." + tools: [] + tasks: + report_task: + agent: reporter + description: "Task Status: {final_summary}" + expected_output: "A final status report." resolved_inputs: - summary_introduction: + final_summary: case: - # Case 1: Check if the deep analysis crew was successful - - condition: - crew: deep_analysis_crew - status: SUCCESS - # If so, use this value for the {summary_introduction} placeholder - value: "A full, deep analysis was performed. The findings were: {deep_analysis_crew}" - # Case 2: Check if the deep analysis crew was skipped - condition: - crew: deep_analysis_crew - status: SKIPPED - value: "A deep analysis was not required based on the triage decision: {triage_crew}" - # A fallback default value if no cases match - default: "Summarize the results of the workflow." + crew: approval_crew + output_contains: "APPROVE" + value: "The process was successfully APPROVED." + default: "The process was NOT approved." +``` + +----- + +### 7\. Other Templating Features + +#### Using External Files with `context` + +Keep your YAML clean by loading long prompts from external files in a `context/` sub-folder. This example loads `context/instructions.txt` into the `{instructions}` placeholder. + +```yaml +crews: + follower_crew: + context: + instructions: 'instructions.txt' + agents: + follower: + role: "Follower" + goal: "Follow instructions." + tools: [] + tasks: + follow_task: + agent: follower + description: "Execute these instructions:\n{instructions}" +``` + +#### Deterministic Filenames with `{sha256:...}` + +Create consistent filenames based on the hash of an input. The output filename will be the same every time the same `report_id` is used. + +```yaml +user_inputs: + report_id: + title: "Unique ID for the report" +crews: + report_crew: + output_naming_template: 'report_{sha256:report_id}.md' + agents: + reporter: + role: "Reporter" + goal: "Generate a report." + tools: [] + tasks: + report_task: + agent: reporter + description: "Generate the report for ID: {report_id}" ``` ### Project Folder Structure diff --git a/crews_control.py b/crews_control.py index 59f2e4c..3a61196 100644 --- a/crews_control.py +++ b/crews_control.py @@ -109,6 +109,9 @@ def main(): except FileNotFoundError: display_error(f"{EXECUTION_CONFIG_PATH} file not found for project {runtime_settings.project_name}") + if 'item' in execution_config.get('user_inputs', {}): + display_error("The user input 'item' is a reserved keyword for the 'for_each' feature. Please choose a different name.") + display_message(f"Welcome to {runtime_settings.project_name}™") try: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..c7e4d4c --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +# Use the HOST_USER_ID and HOST_GROUP_ID passed in, or default to 1000 +HOST_USER_ID=${HOST_USER_ID:-1000} +HOST_GROUP_ID=${HOST_GROUP_ID:-1000} + +# Modify the appuser's UID and GID to match the host user. +# This ensures that files created in mounted volumes have the correct ownership. +groupmod -g ${HOST_GROUP_ID} -o appuser +usermod -u ${HOST_USER_ID} -o appuser + +# Now, drop root privileges and execute the command passed to this script (the Dockerfile CMD) +# as the correctly-mapped 'appuser'. +exec gosu appuser "$@" diff --git a/execution/crews/builder.py b/execution/crews/builder.py index 18d9cdb..60a6411 100644 --- a/execution/crews/builder.py +++ b/execution/crews/builder.py @@ -44,7 +44,8 @@ def __init__( self._crew_config: dict = crew_config self._project_name: str = project_name self._previous_results: dict = previous_crews_results # Contains {'status': '...', 'output': '...'} - self._llm, self._embedding_model = llm, embedding_model + self._llm_clients: Dict[str, Any] = {'default': llm} # The 'llm' passed in is the default client, stored in a cache. + self._embedding_model = embedding_model self._crew_context: typing.Optional[dict] = None self._ignore_cache: bool = ignore_cache @@ -70,6 +71,59 @@ def __init__( # validate crew parameters (agents/tasks presence) self.validate_crew_parameters() + def _get_llm_client(self, model_config: Optional[dict] = None) -> Any: + """ + Gets an LLM client based on a model configuration object from the YAML. + If no config is provided, returns the default client. Caches clients for reuse. + """ + if not model_config: + return self._llm_clients['default'] + + provider = model_config.get('provider') + if not provider: + rich.print(f"[bold red]Error: 'llm_model' config for an agent is missing the 'provider' key. Using default LLM.[/bold red]") + return self._llm_clients['default'] + + model_name = model_config.get('model_name') + cache_key = f"{provider}-{model_name}" if model_name else provider + + if cache_key in self._llm_clients: + rich.print(f"[blue]Using cached LLM client for: {cache_key}[/blue]") + return self._llm_clients[cache_key] + + rich.print(f"[yellow]Initializing new LLM client for: {cache_key}...[/yellow]") + try: + llm_config_path = Path('config') / 'llms' / f'{provider}.json' + if not llm_config_path.exists(): + raise FileNotFoundError(f"LLM config file not found for provider '{provider}' at {llm_config_path}") + + base_config = load_config(llm_config_path) + + # Call the factory with the base config and the specific overrides from the YAML + new_client = create_llm_client(base_config, overrides=model_config) + + self._llm_clients[cache_key] = new_client + return new_client + + except Exception as e: + rich.print(f"[bold red]Error: Failed to create LLM client for '{cache_key}'. Using default LLM as fallback. Error: {e}[/bold red]") + return self._llm_clients['default'] + + def _check_output_condition(self, condition: dict, output_text: str) -> bool: + """Evaluates if the output text meets all specified conditions.""" + checks = [] + + if 'output_contains' in condition: + expected = condition['output_contains'] + checks.append(expected.strip().upper() in output_text.strip().upper()) + + if 'output_not_contains' in condition: + forbidden = condition['output_not_contains'] + checks.append(forbidden.strip().upper() not in output_text.strip().upper()) + + # All conditions must be satisfied (logical AND) + return all(checks) if checks else True + def _parse_and_get_tools(self, tools_config: list, tool_scope: typing.Optional[str] = None) -> list: """Parses the tool configuration from YAML and returns a list of instantiated tool objects.""" if not tools_config: @@ -284,6 +338,9 @@ def _get_agent(self, agent_name: str, agent_scope: typing.Optional[str] = None) tool_scope=agent_scope ) + agent_llm_config = agent_config.get('llm_model') # Get the specific LLM configuration object for this agent + agent_llm_client = self._get_llm_client(agent_llm_config) # and fetch the corresponding LLM client + # Agent role, goal, backstory are evaluated here. # These should use the *full* context including resolved inputs if they are configured for agents. # Assuming agent_scope implies task_name for resolved_inputs @@ -293,7 +350,7 @@ def _get_agent(self, agent_name: str, agent_scope: typing.Optional[str] = None) tools=agent_tools, backstory=self._evaluate_input(agent_config['backstory'], task_name=agent_scope), allow_delegation=False, - llm=self._llm, + llm=agent_llm_client, embedding_model=self._embedding_model, verbose=True, memory=True, @@ -378,12 +435,53 @@ def _get_export_path(self) -> Path: return Path.cwd() / 'projects' / self._project_name / 'output' / self._output_file def run_crew(self) -> str: - export_path: Path = self._get_export_path() - if not self._ignore_cache and export_path.exists(): - cached_content = export_path.read_text() - rich.print(f"[yellow bold]Using cached result for <{self._crew_name}>[/yellow bold]") - return cached_content # Return the plain string directly from cache + run_until_config = self._crew_config.get('run_until') + if not run_until_config: + export_path: Path = self._get_export_path() + if not self._ignore_cache and export_path.exists(): + cached_content = export_path.read_text() + rich.print(f"[yellow bold]Using cached result for <{self._crew_name}>[/yellow bold]") + return cached_content # Return the plain string directly from cache + return self._execute_crew_with_error_handling() + + max_retries = run_until_config.get('max_retries', 3) + if max_retries == 0: + raise ValueError("max_retries=0 is invalid. To run once, remove the 'run_until' block entirely.") + if max_retries < -1: + raise ValueError("max_retries must be -1 for infinite retries or a positive integer (>= 1) for limited retries.") + + delay = run_until_config.get('delay_seconds', 0) + condition = run_until_config.get('condition', {}) + rich.print(f"[cyan bold]Crew <{self._crew_name}> will run until condition is met (max {max_retries} retries).[/cyan bold]") + + attempt = 0 + final_result_raw = "" + + while max_retries == -1 or attempt < max_retries: + attempt += 1 + rich.print(f"[cyan]Attempt {attempt}/{max_retries} for crew <{self._crew_name}>...[/cyan]") + self._ignore_cache = True # Force cache to be ignored during looping + result_raw = self._execute_crew_with_error_handling() + final_result_raw = result_raw # Always store the latest result + if self._check_output_condition(condition, result_raw): + rich.print(f"[green bold]Condition met for <{self._crew_name}>. Proceeding.[/green bold]") + break # Exit the loop on success + + rich.print(f"[yellow]Condition not met for <{self._crew_name}>.[/yellow]") + if attempt < max_retries: + if delay > 0: + rich.print(f"[yellow]Waiting {delay} seconds before next attempt...[/yellow]") + time.sleep(delay) + else: + rich.print(f"[red bold]Max retries reached for <{self._crew_name}>. Using the last result.[/red bold]") + + final_output_obj = CrewOutput(raw=final_result_raw, pydantic_output=None, tasks_output=[]) + self._export_results(final_output_obj) + + return final_result_raw + def _execute_crew_with_error_handling(self) -> str: + """Encapsulates the core crew execution and transient error retries.""" max_retries = 5 retry_count = 0 backoff_factor = 2 @@ -395,9 +493,13 @@ def run_crew(self) -> str: tasks=self._get_crew_tasks(), verbose=True ).kickoff() - self._export_results(results) - return results.raw + + # In the looping case, the final export is handled outside this method. + # In the single-run case, this export is the one that runs. + if not self._crew_config.get('run_until'): + self._export_results(results) + return results.raw except Exception as e: error_code = self._extract_error_code(e) rich.print(f"[red bold]Error occurred while running crew <{self._crew_name}>[/red bold]") @@ -412,10 +514,10 @@ def run_crew(self) -> str: os._exit(1) return str(e) - rich.print(f"[red bold]Exceeded maximum retries. Aborting...[/bold red]") + rich.print(f"[red bold]Exceeded maximum retries for transient errors. Aborting...[/bold red]") return "Rate limit error: Exceeded maximum retries" def _extract_error_code(self, exception: Exception) -> str: if hasattr(exception, 'response') and hasattr(exception.response, 'status_code'): return str(exception.response.status_code) - return "" \ No newline at end of file + return "" diff --git a/execution/orchestrator.py b/execution/orchestrator.py index dd8690a..1e7c2e1 100644 --- a/execution/orchestrator.py +++ b/execution/orchestrator.py @@ -97,42 +97,101 @@ def execute_crews(project_name: str, } continue - rich.print(f"[white bold]Running crew <{acting_crew}> [/white bold]") - try: - crew_run_raw_or_obj_result: Union[CrewOutput, str] = CrewRunner( - project_name=project_name, - crew_name=acting_crew, - crew_config=crew_config, - user_inputs=user_inputs, - previous_crews_results=crews_results, # Pass the full structured results - llm=llm, - embedding_model=embedding_model, - should_export_results=(execution_config.get('settings') or {}).get('output_results'), - ignore_cache=ignore_cache, - guardrail_verbose_logging=True, - - ).run_crew() - if isinstance(crew_run_raw_or_obj_result, CrewOutput): - # If it's a CrewOutput object, get its raw string content - result_output: str = crew_run_raw_or_obj_result.raw - else: - # Otherwise, it's already a string (from cache, or an error message string) - result_output: str = str(crew_run_raw_or_obj_result) # Ensure it's a string just in case - # Wrap the successful result in the new structure - crews_results[acting_crew] = { - "status": "SUCCESS", - "output": result_output + # Check if the crew should be run as an iterator + if 'for_each' in crew_config: + rich.print(f"[cyan bold]Executing iterator crew <{acting_crew}>[/cyan bold]") + + list_source_template = crew_config['for_each'] + + # Build the context for formatting from previous results and user inputs + formatting_context = { + crew_name: result.get('output', '') + for crew_name, result in crews_results.items() } + formatting_context.update(user_inputs) - except Exception as e: - # Handle unexpected failures during crew execution - rich.print(f"[bold red]An unexpected error occurred while running crew <{acting_crew}>: {e}[/bold red]") - crews_results[acting_crew] = { - "status": "FAILED", - "output": str(e) - } - if os.getenv('EXIT_ON_ERROR', 'False').lower() == 'true': - os._exit(1) + try: + # Evaluate the template to get the final string, then parse as JSON + evaluated_list_string = list_source_template.format(**formatting_context) + items_to_iterate = json.loads(evaluated_list_string) + + if not isinstance(items_to_iterate, list): + raise TypeError("The evaluated 'for_each' template must result in a JSON list.") + + except (json.JSONDecodeError, TypeError, KeyError) as e: + error_msg = f"Failed to resolve 'for_each' for crew <{acting_crew}>. The template or source output was invalid. Error: {e}" + rich.print(f"[bold red]{error_msg}[/bold red]") + crews_results[acting_crew] = {"status": "FAILED", "output": error_msg} + continue + + iteration_results = [] + for index, item in enumerate(items_to_iterate): + rich.print(f"[cyan] - Running iteration {index + 1}/{len(items_to_iterate)} for <{acting_crew}>[/cyan]") + + # Inject the current item into the inputs for this specific run + iteration_user_inputs = user_inputs.copy() + iteration_user_inputs['item'] = item + + # The iterator crew runs its own definition for each item + try: + result: Union[CrewOutput, str] = CrewRunner( + project_name=project_name, + crew_name=f"{acting_crew}_iteration_{index}", # Dynamic name for caching + crew_config=crew_config, # Use its own config + user_inputs=iteration_user_inputs, + previous_crews_results=crews_results, + llm=llm, + embedding_model=embedding_model, + should_export_results=(execution_config.get('settings') or {}).get('output_results'), + ignore_cache=ignore_cache, + ).run_crew() + iteration_results.append(result) + except Exception as e: + error_msg = f"Error in iteration {index} for crew <{acting_crew}>: {e}" + rich.print(f"[bold red]{error_msg}[/bold red]") + iteration_results.append({"error": error_msg}) + + # Aggregate all iteration results into the output for the main iterator crew + crews_results[acting_crew] = {"status": "SUCCESS", "output": json.dumps(iteration_results, indent=2)} + + else: + # running a standard, non-iterating crew + rich.print(f"[white bold]Running crew <{acting_crew}> [/white bold]") + try: + crew_run_raw_or_obj_result: Union[CrewOutput, str] = CrewRunner( + project_name=project_name, + crew_name=acting_crew, + crew_config=crew_config, + user_inputs=user_inputs, + previous_crews_results=crews_results, # Pass the full structured results + llm=llm, + embedding_model=embedding_model, + should_export_results=(execution_config.get('settings') or {}).get('output_results'), + ignore_cache=ignore_cache, + guardrail_verbose_logging=True, + + ).run_crew() + if isinstance(crew_run_raw_or_obj_result, CrewOutput): + # If it's a CrewOutput object, get its raw string content + result_output: str = crew_run_raw_or_obj_result.raw + else: + # Otherwise, it's already a string (from cache, or an error message string) + result_output: str = str(crew_run_raw_or_obj_result) # Ensure it's a string just in case + # Wrap the successful result in the new structure + crews_results[acting_crew] = { + "status": "SUCCESS", + "output": result_output + } + + except Exception as e: + # Handle unexpected failures during crew execution + rich.print(f"[bold red]An unexpected error occurred while running crew <{acting_crew}>: {e}[/bold red]") + crews_results[acting_crew] = { + "status": "FAILED", + "output": str(e) + } + if os.getenv('EXIT_ON_ERROR', 'False').lower() == 'true': + os._exit(1) if validations and acting_crew in validations: # First, ensure the crew we want to validate was actually successful diff --git a/projects/threat-model/execution.yaml b/projects/threat-model/execution.yaml index a2048f8..239df19 100644 --- a/projects/threat-model/execution.yaml +++ b/projects/threat-model/execution.yaml @@ -16,12 +16,12 @@ crews: tools: - name: jira_get_issue_details result_as_answer: true - custom_field_names_to_fetch: - - "Team" # Example: Add the custom fields you want to fetch - - "Target Version" - epic_issue_type_names: - - "Epic" # Example: List all names your Jira instance uses for epics - - "Initiative" + custom_field_names_to_fetch: + - "Team" # Example: Add the custom fields you want to fetch + - "Target Version" + epic_issue_type_names: + - "Epic" # Example: List all names your Jira instance uses for epics + - "Initiative" backstory: > Specialized in fetching detailed information from Jira tickets using their ID. Provides a complete JSON representation of the ticket for downstream processing. @@ -131,12 +131,12 @@ crews: and Confluence pages linked within provided text. tools: - name: jira_get_issue_details - custom_field_names_to_fetch: - - "Team" - - "Target Version" - epic_issue_type_names: - - "Epic" # Example: List all names your Jira instance uses for epics - - "Initiative" + custom_field_names_to_fetch: + - "Team" + - "Target Version" + epic_issue_type_names: + - "Epic" # Example: List all names your Jira instance uses for epics + - "Initiative" - confluence - image_analyzer_tool backstory: > diff --git a/requirements.in b/requirements.in index 0ed5cd6..aea1594 100644 --- a/requirements.in +++ b/requirements.in @@ -6,7 +6,7 @@ crewai>=0.134.0 duckduckgo-search atlassian-python-api pytesseract -Pillow +Pillow>=11.3.0 pdf2image markdownify rich diff --git a/requirements.txt b/requirements.txt index 97602b2..5b6fc4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -738,7 +738,9 @@ greenlet==3.2.3 \ --hash=sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a \ --hash=sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712 \ --hash=sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728 - # via -r requirements.in + # via + # -r requirements.in + # sqlalchemy groq==0.29.0 \ --hash=sha256:03515ec46be1ef1feef0cd9d876b6f30a39ee2742e76516153d84acd7c97f23a \ --hash=sha256:109dc4d696c05d44e4c2cd157652c4c6600c3e96f093f6e158facb5691e37847 @@ -1629,6 +1631,97 @@ numpy==2.3.1 \ # scikit-learn # scipy # transformers +nvidia-cublas-cu12==12.6.4.1 \ + --hash=sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb \ + --hash=sha256:235f728d6e2a409eddf1df58d5b0921cf80cfa9e72b9f2775ccb7b4a87984668 \ + --hash=sha256:9e4fa264f4d8a4eb0cdbd34beadc029f453b3bafae02401e999cf3d5a5af75f8 + # via + # nvidia-cudnn-cu12 + # nvidia-cusolver-cu12 + # torch +nvidia-cuda-cupti-cu12==12.6.80 \ + --hash=sha256:166ee35a3ff1587f2490364f90eeeb8da06cd867bd5b701bf7f9a02b78bc63fc \ + --hash=sha256:358b4a1d35370353d52e12f0a7d1769fc01ff74a191689d3870b2123156184c4 \ + --hash=sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132 \ + --hash=sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73 \ + --hash=sha256:bbe6ae76e83ce5251b56e8c8e61a964f757175682bbad058b170b136266ab00a + # via torch +nvidia-cuda-nvrtc-cu12==12.6.77 \ + --hash=sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53 \ + --hash=sha256:5847f1d6e5b757f1d2b3991a01082a44aad6f10ab3c5c0213fa3e25bddc25a13 \ + --hash=sha256:f7007dbd914c56bd80ea31bc43e8e149da38f68158f423ba845fc3292684e45a + # via torch +nvidia-cuda-runtime-cu12==12.6.77 \ + --hash=sha256:6116fad3e049e04791c0256a9778c16237837c08b27ed8c8401e2e45de8d60cd \ + --hash=sha256:86c58044c824bf3c173c49a2dbc7a6c8b53cb4e4dca50068be0bf64e9dab3f7f \ + --hash=sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8 \ + --hash=sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7 \ + --hash=sha256:d461264ecb429c84c8879a7153499ddc7b19b5f8d84c204307491989a365588e + # via torch +nvidia-cudnn-cu12==9.5.1.17 \ + --hash=sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2 \ + --hash=sha256:9fd4584468533c61873e5fda8ca41bac3a38bcb2d12350830c69b0a96a7e4def \ + --hash=sha256:d7af0f8a4f3b4b9dbb3122f2ef553b45694ed9c384d5a75bab197b8eefb79ab8 + # via torch +nvidia-cufft-cu12==11.3.0.4 \ + --hash=sha256:6048ebddfb90d09d2707efb1fd78d4e3a77cb3ae4dc60e19aab6be0ece2ae464 \ + --hash=sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca \ + --hash=sha256:8510990de9f96c803a051822618d42bf6cb8f069ff3f48d93a8486efdacb48fb \ + --hash=sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5 \ + --hash=sha256:d16079550df460376455cba121db6564089176d9bac9e4f360493ca4741b22a6 + # via torch +nvidia-cufile-cu12==1.11.1.6 \ + --hash=sha256:8f57a0051dcf2543f6dc2b98a98cb2719c37d3cee1baba8965d57f3bbc90d4db \ + --hash=sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159 + # via torch +nvidia-curand-cu12==10.3.7.77 \ + --hash=sha256:6d6d935ffba0f3d439b7cd968192ff068fafd9018dbf1b85b37261b13cfc9905 \ + --hash=sha256:6e82df077060ea28e37f48a3ec442a8f47690c7499bff392a5938614b56c98d8 \ + --hash=sha256:7b2ed8e95595c3591d984ea3603dd66fe6ce6812b886d59049988a712ed06b6e \ + --hash=sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117 \ + --hash=sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf + # via torch +nvidia-cusolver-cu12==11.7.1.2 \ + --hash=sha256:0ce237ef60acde1efc457335a2ddadfd7610b892d94efee7b776c64bb1cac9e0 \ + --hash=sha256:6813f9d8073f555444a8705f3ab0296d3e1cb37a16d694c5fc8b862a0d8706d7 \ + --hash=sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6 \ + --hash=sha256:dbbe4fc38ec1289c7e5230e16248365e375c3673c9c8bac5796e2e20db07f56e \ + --hash=sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c + # via torch +nvidia-cusparse-cu12==12.5.4.2 \ + --hash=sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f \ + --hash=sha256:4acb8c08855a26d737398cba8fb6f8f5045d93f82612b4cfd84645a2332ccf20 \ + --hash=sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73 \ + --hash=sha256:7aa32fa5470cf754f72d1116c7cbc300b4e638d3ae5304cfa4a638a5b87161b1 \ + --hash=sha256:d25b62fb18751758fe3c93a4a08eff08effedfe4edf1c6bb5afd0890fe88f887 + # via + # nvidia-cusolver-cu12 + # torch +nvidia-cusparselt-cu12==0.6.3 \ + --hash=sha256:3b325bcbd9b754ba43df5a311488fca11a6b5dc3d11df4d190c000cf1a0765c7 \ + --hash=sha256:8371549623ba601a06322af2133c4a44350575f5a3108fb75f3ef20b822ad5f1 \ + --hash=sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46 + # via torch +nvidia-nccl-cu12==2.26.2 \ + --hash=sha256:5c196e95e832ad30fbbb50381eb3cbd1fadd5675e587a548563993609af19522 \ + --hash=sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6 + # via torch +nvidia-nvjitlink-cu12==12.6.85 \ + --hash=sha256:cf4eaa7d4b6b543ffd69d6abfb11efdeb2db48270d94dfd3a452c24150829e41 \ + --hash=sha256:e61120e52ed675747825cdd16febc6a0730537451d867ee58bee3853b1b13d1c \ + --hash=sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a + # via + # nvidia-cufft-cu12 + # nvidia-cusolver-cu12 + # nvidia-cusparse-cu12 + # torch +nvidia-nvtx-cu12==12.6.77 \ + --hash=sha256:2fb11a4af04a5e6c84073e6404d26588a34afd35379f0855a99797897efa75c0 \ + --hash=sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1 \ + --hash=sha256:adcaabb9d436c9761fca2b13959a2d237c5f9fd406c8e4b723c695409ff88059 \ + --hash=sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2 \ + --hash=sha256:f44f8d86bb7d5629988d61c8d3ae61dddb2015dee142740536bc7481b022fe4b + # via torch oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 @@ -1835,88 +1928,113 @@ pexpect==4.9.0 \ --hash=sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 \ --hash=sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f # via ipython -pillow==11.2.1 \ - --hash=sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928 \ - --hash=sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b \ - --hash=sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91 \ - --hash=sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97 \ - --hash=sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4 \ - --hash=sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193 \ - --hash=sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95 \ - --hash=sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941 \ - --hash=sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f \ - --hash=sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f \ - --hash=sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3 \ - --hash=sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044 \ - --hash=sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb \ - --hash=sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681 \ - --hash=sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d \ - --hash=sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2 \ - --hash=sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb \ - --hash=sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d \ - --hash=sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406 \ - --hash=sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70 \ - --hash=sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79 \ - --hash=sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e \ - --hash=sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013 \ - --hash=sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d \ - --hash=sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2 \ - --hash=sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36 \ - --hash=sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7 \ - --hash=sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751 \ - --hash=sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c \ - --hash=sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c \ - --hash=sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c \ - --hash=sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b \ - --hash=sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1 \ - --hash=sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd \ - --hash=sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8 \ - --hash=sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691 \ - --hash=sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14 \ - --hash=sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b \ - --hash=sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f \ - --hash=sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0 \ - --hash=sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed \ - --hash=sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0 \ - --hash=sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909 \ - --hash=sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22 \ - --hash=sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788 \ - --hash=sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16 \ - --hash=sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156 \ - --hash=sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad \ - --hash=sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076 \ - --hash=sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7 \ - --hash=sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e \ - --hash=sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6 \ - --hash=sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772 \ - --hash=sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155 \ - --hash=sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830 \ - --hash=sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67 \ - --hash=sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4 \ - --hash=sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61 \ - --hash=sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8 \ - --hash=sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01 \ - --hash=sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e \ - --hash=sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1 \ - --hash=sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d \ - --hash=sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579 \ - --hash=sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6 \ - --hash=sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1 \ - --hash=sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7 \ - --hash=sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047 \ - --hash=sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443 \ - --hash=sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a \ - --hash=sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf \ - --hash=sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd \ - --hash=sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193 \ - --hash=sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600 \ - --hash=sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c \ - --hash=sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363 \ - --hash=sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e \ - --hash=sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35 \ - --hash=sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9 \ - --hash=sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28 \ - --hash=sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b +pillow==11.3.0 \ + --hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \ + --hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \ + --hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \ + --hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \ + --hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \ + --hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \ + --hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \ + --hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \ + --hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \ + --hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \ + --hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \ + --hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \ + --hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \ + --hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \ + --hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \ + --hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \ + --hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \ + --hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \ + --hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \ + --hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \ + --hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \ + --hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \ + --hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \ + --hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \ + --hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \ + --hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \ + --hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \ + --hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \ + --hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \ + --hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \ + --hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \ + --hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \ + --hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \ + --hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \ + --hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \ + --hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \ + --hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \ + --hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \ + --hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \ + --hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \ + --hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \ + --hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \ + --hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \ + --hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \ + --hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \ + --hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \ + --hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \ + --hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \ + --hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \ + --hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \ + --hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \ + --hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \ + --hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \ + --hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \ + --hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \ + --hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \ + --hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \ + --hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \ + --hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \ + --hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \ + --hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \ + --hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \ + --hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \ + --hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \ + --hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \ + --hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \ + --hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \ + --hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \ + --hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \ + --hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \ + --hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \ + --hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \ + --hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \ + --hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \ + --hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \ + --hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \ + --hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \ + --hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \ + --hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \ + --hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \ + --hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \ + --hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \ + --hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \ + --hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \ + --hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \ + --hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \ + --hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \ + --hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \ + --hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \ + --hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \ + --hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \ + --hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \ + --hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \ + --hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \ + --hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \ + --hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \ + --hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \ + --hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \ + --hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \ + --hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \ + --hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \ + --hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \ + --hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \ + --hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \ + --hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \ + --hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3 # via # -r requirements.in # jira @@ -3129,6 +3247,14 @@ transformers==4.52.4 \ # via # -r requirements.in # sentence-transformers +triton==3.3.1 \ + --hash=sha256:9999e83aba21e1a78c1f36f21bce621b77bcaa530277a50484a7cb4a822f6e43 \ + --hash=sha256:a3198adb9d78b77818a5388bff89fa72ff36f9da0bc689db2f0a651a67ce6a42 \ + --hash=sha256:b31e3aa26f8cb3cc5bf4e187bf737cbacf17311e1112b781d4a059353dfd731b \ + --hash=sha256:b74db445b1c562844d3cfad6e9679c72e93fdfb1a90a24052b03bb5c49d1242e \ + --hash=sha256:b89d846b5a4198317fec27a5d3a609ea96b6d557ff44b56c23176546023c4240 \ + --hash=sha256:f6139aeb04a146b0b8e0fbbd89ad1e65861c57cfed881f21d62d3cb94a36bab7 + # via torch typer==0.16.0 \ --hash=sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855 \ --hash=sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b diff --git a/tools/custom/image_analyzer_tool.py b/tools/custom/image_analyzer_tool.py index 7aca670..0b58a19 100644 --- a/tools/custom/image_analyzer_tool.py +++ b/tools/custom/image_analyzer_tool.py @@ -5,7 +5,7 @@ import logging # Use standard logging import http.client # Keep for optional debugging setup from io import BytesIO -from typing import Dict, Literal, Optional, Type, Any, Tuple, List +from typing import Dict, Literal, Optional, Type, Any, Tuple, List, Union from pydantic import BaseModel, Field, field_validator, model_validator, SecretStr, HttpUrl, ConfigDict from urllib.parse import urlparse @@ -110,6 +110,19 @@ def init_client(self) -> 'AzureConfig': raise ConnectionError(f"Failed to initialize Azure OpenAI client: {e}") from e return self +class OpenAIConfig(BaseModel): + """Configuration for standard OpenAI Vision API.""" + api_key: SecretStr + model: str + client: Optional[Any] = None + + @model_validator(mode='after') + def init_client(self) -> 'OpenAIConfig': + if not OPENAI_AVAILABLE: + raise ImportError("Cannot initialize OpenAI client: 'openai' library is required.") + logger.info(f"Initializing standard OpenAI client for model: {self.model}") + self.client = openai.OpenAI(api_key=self.api_key.get_secret_value()) + return self class JiraConfig(BaseModel): instance_url: HttpUrl @@ -226,13 +239,13 @@ class AdvancedImageAnalyzerTool(BaseTool): args_schema: type[BaseModel] = AdvancedImageAnalyzerSchema # Configuration stored from init - _azure_config: Optional[AzureConfig] = None + _vision_config: Union[AzureConfig, OpenAIConfig] _jira_config: Optional[JiraConfig] = None _confluence_config: Optional[ConfluenceConfig] = None def __init__( self, - azure_config: AzureConfig, # Require Azure config for analysis + vision_config: Union[AzureConfig, OpenAIConfig], jira_config: Optional[JiraConfig] = None, confluence_config: Optional[ConfluenceConfig] = None, **kwargs @@ -241,7 +254,7 @@ def __init__( Initializes the tool with necessary configurations. Args: - azure_config: Configuration for Azure OpenAI Vision API. + vision_config: A configuration object for either Azure or OpenAI. jira_config: Optional configuration for Jira access. Required if analyzing Jira attachments. confluence_config: Optional configuration for Confluence access. Required if analyzing Confluence attachments. """ @@ -251,11 +264,10 @@ def __init__( self._logger.info("Initializing AdvancedImageAnalyzerTool...") # --- Validate and Store Configurations --- - if not isinstance(azure_config, AzureConfig) or not azure_config.client: - # Client initialization happens within AzureConfig validation - raise ToolConfigurationError("Valid AzureConfig with initialized client is required.") - self._azure_config = azure_config - self._logger.info("Azure configuration loaded.") + if not vision_config or not vision_config.client: + raise ToolConfigurationError("A valid and initialized vision configuration (AzureConfig or OpenAIConfig) is required.") + self._vision_config = vision_config + self._logger.info("Vision configuration loaded.") if jira_config: if not JIRA_AVAILABLE: @@ -502,54 +514,52 @@ def _to_base64_data_uri(self, image_bytes: bytes) -> str: self._logger.info(f"Encoded image to base64 data URI (MIME: {mime_type}, Length: {len(data_uri)}).") return data_uri - def _call_azure_vision_api(self, data_uri: str, prompt: str) -> str: - """Calls the configured Azure Vision API.""" - if not self._azure_config or not self._azure_config.client: - # This should be caught at init, but double-check - raise ToolConfigurationError("Azure Vision API client is not configured.") - - client = self._azure_config.client - deployment = self._azure_config.vision_deployment - self._logger.info(f"Calling Azure Vision API (Deployment: {deployment}). Prompt: '{prompt[:100]}...'") + def _call_vision_api(self, data_uri: str, prompt: str) -> str: + """Dynamically calls the correct vision API based on configuration.""" + client = self._vision_config.client + + if isinstance(self._vision_config, AzureConfig): + model_name = self._vision_config.vision_deployment + self._logger.info(f"Calling Azure Vision API (Deployment: {model_name}).") + elif isinstance(self._vision_config, OpenAIConfig): + model_name = self._vision_config.model + self._logger.info(f"Calling standard OpenAI Vision API (Model: {model_name}).") + else: + raise ToolConfigurationError("Invalid vision API configuration provided.") try: - api_response = client.chat.completions.create( - model=deployment, - messages=[ - { - "role": "user", - "content": [ - {"type": "text", "text": prompt}, - {"type": "image_url", "image_url": {"url": data_uri}}, - ], - } - ], - # max_completion_tokens=1500, + response = client.chat.completions.create( + model=model_name, + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + {"type": "image_url", "image_url": {"url": data_uri}}, + ], + }], + max_tokens=4096 ) - - description = api_response.choices[0].message.content + + description = response.choices[0].message.content if not description: - self._logger.warning("Received empty description from Vision API.") - # Decide if empty response is an error or valid result - # raise VisionApiError("Received empty description from Vision API.") - return "(Vision API returned an empty description)" # Or return specific string - + self._logger.warning("Received empty description from Vision API.") + return "(Vision API returned an empty description)" + self._logger.info("Received description from Vision API successfully.") return description.strip() except openai.APIConnectionError as e: - self._logger.error(f"Azure OpenAI connection error: {e}", exc_info=True) - raise VisionApiError(f"Could not connect to Azure OpenAI: {e}") from e + self._logger.error(f"API connection error: {e}", exc_info=True) + raise VisionApiError(f"Could not connect to the API: {e}") from e except openai.RateLimitError as e: - self._logger.error(f"Azure OpenAI rate limit exceeded: {e}", exc_info=False) - raise VisionApiError(f"Azure OpenAI rate limit exceeded. Please try again later.") from e + self._logger.error(f"API rate limit exceeded: {e}", exc_info=False) + raise VisionApiError(f"API rate limit exceeded. Please try again later.") from e except openai.APIStatusError as e: - self._logger.error(f"Azure OpenAI API error: Status={e.status_code}, Response={e.response}", exc_info=True) - raise VisionApiError(f"Azure OpenAI API returned an error (Status {e.status_code}). Check deployment name and API key/endpoint.") from e + self._logger.error(f"API status error: Status={e.status_code}, Response={e.response}", exc_info=True) + raise VisionApiError(f"API returned an error (Status {e.status_code}). Check deployment name and API key/endpoint.") from e except Exception as e: - self._logger.error(f"Unexpected error during Vision API call: {e}", exc_info=True) - raise VisionApiError(f"An unexpected error occurred during image analysis: {e}") from e - + self._logger.error(f"Unexpected error during Vision API call: {e}", exc_info=True) + raise VisionApiError(f"An unexpected error occurred during image analysis: {e}") from e # --- Main Execution Method (`_run`) --- def _run( @@ -597,7 +607,7 @@ def _run( data_uri = self._to_base64_data_uri(image_bytes) del image_bytes - description = self._call_azure_vision_api(data_uri, analysis_prompt) + description = self._call_vision_api(data_uri, analysis_prompt) self._logger.info(f"Analysis successful for '{reference}'.") all_results.append(f"Result for '{reference}':\n{description}") @@ -781,4 +791,4 @@ def run_crewai_example(): # --- Main Execution Guard --- if __name__ == "__main__": - run_crewai_example() \ No newline at end of file + run_crewai_example() diff --git a/tools/custom/jira_fetch_ticket_details_tool.py b/tools/custom/jira_fetch_ticket_details_tool.py index 1ffc002..be93faf 100644 --- a/tools/custom/jira_fetch_ticket_details_tool.py +++ b/tools/custom/jira_fetch_ticket_details_tool.py @@ -2,6 +2,8 @@ import json from jira import JIRA, JIRAError from crewai.tools import BaseTool +from pydantic import Field +from typing import List, Optional def _format_comments(jira_comments_field): """Helper function to format JIRA comments.""" @@ -36,6 +38,9 @@ class JiraTicketDetailsTool(BaseTool): "linked issues, parent, sub-tasks, epic children, and specified custom fields) " "using the ticket ID and returns a JSON-formatted string." ) + + custom_field_names_to_fetch: Optional[List[str]] = Field(default_factory=list) + epic_issue_type_names: Optional[List[str]] = Field(default_factory=lambda: ["Epic"]) # --- INTERNAL CACHE --- _custom_field_id_map: dict[str, str] | None = None @@ -43,24 +48,6 @@ class JiraTicketDetailsTool(BaseTool): class Config: arbitrary_types_allowed = True - def __init__(self, - custom_field_names_to_fetch: list[str] = None, - epic_issue_type_names: list[str] = None, - **kwargs): - """ - Initializes the tool. - Args: - custom_field_names_to_fetch (list[str], optional): - A list of custom field names to fetch from Jira tickets. Defaults to an empty list. - epic_issue_type_names (list[str], optional): - A list of strings that represent the 'Epic' issue type in your Jira instance. - Defaults to ["Epic"]. - """ - super().__init__(**kwargs) - # Set the fields from the arguments, providing sensible defaults. - self.custom_field_names_to_fetch = custom_field_names_to_fetch or [] - self.epic_issue_type_names = epic_issue_type_names or ["Epic"] - def _resolve_custom_field_ids(self, jira_client: JIRA): """ Resolves custom field names to their IDs once and caches them in the instance. diff --git a/tools/index.py b/tools/index.py index 021a623..8e46026 100644 --- a/tools/index.py +++ b/tools/index.py @@ -18,7 +18,10 @@ from tools.custom.fetch_file_content_tool import GitFileContentQueryTool from tools.custom.github_fetch_file_paginated import GitHubFilePaginator from tools.custom.confluence_fetch import ConfluenceDataQueryTool -from tools.custom.image_analyzer_tool import AdvancedImageAnalyzerTool, AzureConfig, JiraConfig, ConfluenceConfig +from tools.custom.image_analyzer_tool import ( + AdvancedImageAnalyzerTool, AzureConfig, OpenAIConfig, JiraConfig, ConfluenceConfig +) + from langchain_community.agent_toolkits.load_tools import load_tools from utils import validate_env_vars, EnvironmentVariableNotSetError @@ -123,6 +126,57 @@ def get_jira_link_pairs() -> list[tuple[str, str]]: allowed_pairs.append(tuple(parts)) return allowed_pairs +def get_image_analyzer_tool(**kwargs): + """ + Initializes the AdvancedImageAnalyzerTool with either Azure or OpenAI vision config, + based on available environment variables. + """ + # Conditionally create Jira and Confluence configs if ENVs are set + jira_config = None + if all(os.getenv(k) for k in ['JIRA_INSTANCE_URL', 'JIRA_USERNAME', 'JIRA_API_TOKEN']): + jira_config = JiraConfig( + instance_url=os.environ['JIRA_INSTANCE_URL'], + username=os.environ['JIRA_USERNAME'], + api_token=os.environ['JIRA_API_TOKEN'] + ) + + confluence_config = None + if all(os.getenv(k) for k in ['CONFLUENCE_ENDPOINT', 'CONFLUENCE_API_USER', 'CONFLUENCE_API_TOKEN']): + confluence_config = ConfluenceConfig( + endpoint_url=os.environ['CONFLUENCE_ENDPOINT'], + username=os.environ['CONFLUENCE_API_USER'], + api_token=os.environ['CONFLUENCE_API_TOKEN'] + ) + + # Decide between Azure and OpenAI for vision based on which ENVs are set + vision_config = None + if os.getenv("AZURE_API_BASE") and os.getenv("AZURE_API_KEY"): + print("INFO: Found Azure environment variables. Initializing Image Analyzer with Azure.") + vision_config = AzureConfig( + api_key=os.environ["AZURE_API_KEY"], + endpoint=os.environ["AZURE_API_BASE"], + api_version=os.environ["AZURE_API_VERSION"], + vision_deployment=os.environ["AZURE_OPENAI_VISION_DEPLOYMENT"] + ) + elif os.getenv("OPENAI_API_KEY") and os.getenv("OPENAI_VISION_MODEL"): + print("INFO: Azure variables not found. Initializing Image Analyzer with standard OpenAI.") + vision_config = OpenAIConfig( + api_key=os.environ["OPENAI_API_KEY"], + model=os.environ["OPENAI_VISION_MODEL"] + ) + else: + # If neither is configured, the tool will not be functional. + # We can let it fail here or allow it to initialize and fail later. + # For now, we let it proceed, and the tool's __init__ will raise an error. + pass + + return AdvancedImageAnalyzerTool( + vision_config=vision_config, + jira_config=jira_config, + confluence_config=confluence_config, + **kwargs + ) + _TOOLS_MAP: dict[str, Callable] = { 'human': lambda: HumanTool(), 'read_file': lambda: load_tools(['read_file'])[0], @@ -164,25 +218,7 @@ def get_jira_link_pairs() -> list[tuple[str, str]]: 'jira_get_issue_details': lambda **kwargs: JiraTicketDetailsTool(**kwargs), 'confluence': lambda **kwargs: ConfluenceDataQueryTool(**kwargs), 'FinalAnswerTool': lambda **kwargs: FinalAnswerTool(**kwargs), - 'image_analyzer_tool': lambda **kwargs: AdvancedImageAnalyzerTool( - AzureConfig( - api_key=os.environ["AZURE_API_KEY"], - endpoint=os.environ["AZURE_API_BASE"], - api_version=os.environ["AZURE_API_VERSION"], - vision_deployment=os.environ["AZURE_OPENAI_VISION_DEPLOYMENT"] - ), - JiraConfig( - instance_url=os.environ['JIRA_INSTANCE_URL'], - username=os.environ['JIRA_USERNAME'], - api_token=os.environ['JIRA_API_TOKEN'] - ), - ConfluenceConfig( - endpoint_url=os.environ['CONFLUENCE_ENDPOINT'], - username=os.environ['CONFLUENCE_API_USER'], - api_token=os.environ['CONFLUENCE_API_TOKEN'] - ), - **kwargs - ), + 'image_analyzer_tool': get_image_analyzer_tool, } class FinalAnswerTool(BaseTool): @@ -202,7 +238,6 @@ def _run(self, final_answer: str) -> str: "JIRA_SETPRIORITY_ALLOWED_PREFIXES", "JIRA_REASSIGN_ALLOWED_PREFIXES", "GITHUB_TOKEN", - "SERPER_API_KEY", "LLM_NAME", "EMBEDDER_NAME", "CONFLUENCE_ENDPOINT", diff --git a/utils.py b/utils.py index cc8add3..673bc93 100644 --- a/utils.py +++ b/utils.py @@ -1,3 +1,4 @@ +from typing import Optional import os from langchain_openai import AzureOpenAIEmbeddings from langchain_openai import AzureChatOpenAI @@ -33,46 +34,53 @@ def validate_env_vars(*vars): if os.getenv(var) is None or os.getenv(var) == "": raise EnvironmentVariableNotSetError(f"Environment variable '{var}' is not set.") -def create_llm_client(config): +def create_llm_client(config: dict, overrides: Optional[dict] = None) -> Any: + if overrides is None: + overrides = {} + provider = config['provider'] validate_env_vars(config['required_vars']) if provider == 'groq': + model = overrides.get('model_name', os.getenv("GROQ_MODEL_NAME")) return ChatGroq( - model=os.getenv("GROQ_MODEL_NAME"), + model_name=model, api_key=os.getenv("GROQ_API_KEY"), streaming=config.get('stream', True), max_tokens=config.get('max_tokens', 8192), - model_name=os.getenv('GROQ_MODEL_NAME'), ) elif provider == 'anthropic': + model = overrides.get('model_name', os.getenv("ANTHROPIC_MODEL_NAME")) + temperature = overrides.get('temperature', 0.7) return ChatAnthropic( - model=os.getenv("ANTHROPIC_MODEL_NAME"), - temperature=config.get('temperature', 0.7), + model=model, + temperature=temperature, max_tokens=config.get('max_tokens', 1024), timeout=None, max_retries=2, ) elif provider == 'azure_openai': from crewai import LLM + deployment = overrides.get('model_name', os.getenv("AZURE_OPENAI_DEPLOYMENT")) return LLM( - model=os.getenv("AZURE_OPENAI_DEPLOYMENT"), + model=deployment, base_url=os.getenv("AZURE_OPENAI_ENDPOINT"), api_version=os.getenv("AZURE_OPENAI_VERSION"), api_key=os.getenv("AZURE_OPENAI_KEY"), - azure=True ) elif provider == 'openai': from langchain_openai import ChatOpenAI + model = overrides.get('model_name', os.getenv("OPENAI_MODEL_NAME")) + temperature = overrides.get('temperature', 0) return ChatOpenAI( - temperature=config.get('temperature', 0), - model=os.getenv("OPENAI_MODEL_NAME"), + temperature=temperature, + model=model, api_key=os.getenv("OPENAI_API_KEY"), ) - # Add more LLM providers here as needed else: raise ValueError(f"Unsupported LLM provider: {provider}") + def create_embedder_client(config): provider = config['provider']