From dcc10976b946d54587f555fb7129586447303a9f Mon Sep 17 00:00:00 2001 From: Avri Schneider Date: Thu, 10 Jul 2025 07:51:28 +0300 Subject: [PATCH 1/5] feat: overhaul Docker workflow and architecture with dynamic LLMs, vision API, and reusable tooling - Refactor GitHub Actions to support automatic build-and-push to GHCR with Buildx caching and disk cleanup - Add new `run-crews-control-project.yaml` workflow for parameterized Docker-based project execution - Redesign Dockerfile for multi-stage, reproducible, non-root builds with dynamic user mapping via entrypoint.sh - Add per-agent LLM configuration via `llm_model` in YAML with lazy caching and validation - Introduce `run_until` crew looping logic with condition-based retries and delay support - Modularize Vision API support (Azure and OpenAI) with dynamic tool selection - Make tools resilient to env var presence; add fallback and validation - Update `README.md` with detailed orchestration examples (dependencies, loops, context, SHA-based filenames) - Ensure updated `requirements.txt` with deterministic hashes for GPU-enabled libraries --- .../build-and-push-docker-image.yaml | 112 +++-- .../workflows/run-crews-control-project.yaml | 122 ++++++ Dockerfile | 67 +-- README.md | 384 ++++++++++++++---- entrypoint.sh | 15 + execution/crews/builder.py | 124 +++++- requirements.in | 2 +- requirements.txt | 292 +++++++++---- tools/custom/image_analyzer_tool.py | 106 ++--- .../custom/jira_fetch_ticket_details_tool.py | 23 +- tools/index.py | 77 +++- utils.py | 28 +- 12 files changed, 1002 insertions(+), 350 deletions(-) create mode 100644 .github/workflows/run-crews-control-project.yaml create mode 100644 entrypoint.sh 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..71357d0 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 and `run_until` loops. +- **[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,306 @@ 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: + 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`). + +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: - gather_task: - agent: data_gatherer_agent - description: "Collect data based on {user_input_1}." - expected_output: "A summary of the gathered data." + analyze_task: + agent: analyzer + description: "Scan {scan_target}. If clean, respond with 'Target is CLEAN'." - triage_crew: + 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\. 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: - summary_agent: - role: "Summarizer" - goal: "Create a final summary of the entire process." + 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: + write_task: + agent: writer + description: "Write a post using these facts:\n{research_crew}" +``` + +----- + +### 5\. 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: - 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 + 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." +``` + +----- + +### 6\. 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/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/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'] From d7c9c83fa371a91d5c4f0633aa565e718a6c0b4c Mon Sep 17 00:00:00 2001 From: Avri Schneider Date: Thu, 10 Jul 2025 20:46:38 +0300 Subject: [PATCH 2/5] Fix indentation --- projects/threat-model/execution.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) 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: > From 784727d2b55c5e07d7109a488cbd7879ca141e0b Mon Sep 17 00:00:00 2001 From: Avri Schneider Date: Fri, 11 Jul 2025 10:26:25 +0300 Subject: [PATCH 3/5] feat(orchestrator): Add 'for_each' crew iterator property Introduces the `for_each` property for crews, enabling dynamic, data-driven iteration within a workflow. When a crew definition includes the `for_each` key, the orchestrator resolves its template string value into a JSON list. The orchestrator then executes the complete crew definition sequentially for each item in that list. The special `{item}` placeholder can be used within the crew's definition (e.g., in `output_naming_template` or a task's `description`) to reference the value of the current iteration. The final output of the iterator crew is an aggregated JSON array containing the result from each run, making it available for downstream processing. --- execution/orchestrator.py | 127 ++++++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 34 deletions(-) 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 From 23efdbf4ea23117fa9cb16a67b9200c15d943712 Mon Sep 17 00:00:00 2001 From: Avri Schneider Date: Fri, 11 Jul 2025 10:32:20 +0300 Subject: [PATCH 4/5] item is now a reserved keyword --- crews_control.py | 3 +++ 1 file changed, 3 insertions(+) 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: From be3ff6ab1fe58a40d0baf250dea80b4cbbde275b Mon Sep 17 00:00:00 2001 From: Avri Schneider Date: Fri, 11 Jul 2025 10:49:03 +0300 Subject: [PATCH 5/5] Update README.md --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 71357d0..a02d33f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ graph TD; ## Features - **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 and `run_until` loops. +- **[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. @@ -407,7 +407,74 @@ crews: * Cached outputs are always ignored during retries to ensure fresh execution. ----- -### 4\. Per-Agent LLM Assignment +### 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: + 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. @@ -449,7 +516,7 @@ crews: ----- -### 5\. Dynamic Task Inputs with `resolved_inputs` +### 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. @@ -494,7 +561,7 @@ crews: ----- -### 6\. Other Templating Features +### 7\. Other Templating Features #### Using External Files with `context`