Compare commits

..

14 Commits

Author SHA1 Message Date
Cursor Agent
4cbf7674cb fix: default empty Responses tool-call arguments to {}
Missing or empty Chat Completions tool-call arguments were forwarded
as an empty string, which is invalid JSON for Responses API
function_call items and breaks parse_tool_call_args. Normalize to
"{}" and cover the case in regression tests. Also assert a supplied
tool-call id is preserved as call_id.

Co-authored-by: Rip&Tear <theCyberTech@users.noreply.github.com>
2026-08-05 10:06:25 +00:00
Cursor Agent
ff47574a39 Merge branch 'main' into fix/native-tool-call-responses-api-shape
Resolve pyproject.toml conflicts by keeping main's stricter
aiohttp>=3.14.3 pin and cryptography advisory comments.

Co-authored-by: Rip&Tear <theCyberTech@users.noreply.github.com>
2026-08-05 10:06:08 +00:00
copilot-swe-agent[bot]
2ded33f027 fix: bump aiohttp to >=3.14.2 and cryptography to >=50.0.0 to fix pip-audit vulns
Co-authored-by: theCyberTech <84775494+theCyberTech@users.noreply.github.com>
2026-08-04 01:29:40 +00:00
Rip&Tear
034658e46c Merge branch 'main' into fix/native-tool-call-responses-api-shape 2026-08-04 09:22:41 +08:00
copilot-swe-agent[bot]
075829c5fb fix: upgrade nltk to 3.10.0 to resolve path traversal vulnerabilities
Upgrades nltk from 3.9.4 to 3.10.0 which fixes three path traversal
vulnerabilities (GHSA-qvv7-cg9c-w4x3, GHSA-fg7f-2386-8897,
GHSA-xh95-f55m-82fw) that were causing the pip-audit CI job to fail.

Also removes the now-obsolete PYSEC-2026-597 ignore entry from the
vulnerability-scan workflow since the vulnerability is fixed in 3.10.0.
2026-08-03 11:31:36 +00:00
Rip&Tear
f9aef7f93c Merge branch 'main' into fix/native-tool-call-responses-api-shape 2026-08-03 19:18:55 +08:00
copilot-swe-agent[bot]
4267e0ffd3 Resolve merge conflicts with origin/main 2026-07-28 04:16:58 +00:00
Rip&Tear
9e06006f72 style: format Responses API conversion 2026-07-13 08:42:38 +08:00
Rip&Tear
f6640b1cd8 fix: harden Responses API tool call conversion 2026-07-13 08:28:38 +08:00
João Moura
6450d67b9c Merge branch 'main' into fix/native-tool-call-responses-api-shape 2026-07-12 17:57:18 -03:00
theCyberTech
6f62ed826d fix(llms/openai): fix mypy list-item type error in message conversion
_convert_message_to_responses_input_items() was annotated to return
list[dict[str, Any]], but the passthrough branch returns the LLMMessage
argument unchanged. Lists are invariant in mypy, so a bare LLMMessage
(TypedDict) isn't assignable into a list[dict[str, Any]] return - this
was flagged by CI's type-checker job across all Python versions.

Widened the return type (and the local list built in the tool_calls
branch) to list[dict[str, Any] | LLMMessage], matching what the
function actually returns.

Confirmed with a local mypy run and the full openai/agent_utils test
suites (176 passed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 19:30:51 +08:00
theCyberTech
37a8267355 fix(llms/openai): convert Chat-Completions tool messages to Responses API input items
_prepare_responses_params() passed non-system messages straight through
as the Responses API "input" array without converting them. That's
fine for plain user/assistant text (matches the API's lenient "easy
input message" shape), but Chat-Completions-style assistant messages
carrying "tool_calls" and "tool"-role messages have no equivalent
shape in the Responses API - it expects standalone "function_call" and
"function_call_output" input items instead. Sending the raw
Chat-Completions shapes gets rejected with a 400 (union-type
validation failure against every Responses API input item variant).

This broke every multi-turn tool-calling conversation over
api="responses" that doesn't rely on auto_chain/previous_response_id
(i.e. the common case: resending full history each turn instead of
referencing server-side state).

Added _convert_message_to_responses_input_items() to translate:
  - assistant + tool_calls -> one function_call item per call
  - tool role               -> function_call_output item
  - everything else         -> passed through unchanged

Verified against a real multi-turn tool-calling run: the agent now
completes the full conversation and returns the actual extracted
answer instead of erroring on the second turn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 18:34:42 +08:00
theCyberTech
cb78402898 test(agent_utils): cover OpenAI Responses API tool-call shape
Regression tests for is_tool_call_list() and extract_tool_call_info()
against the Responses API's flat {"id", "name", "arguments"} dict
shape, alongside existing Chat-Completions and Bedrock/Anthropic
shapes to confirm no regression there.

Confirmed these tests fail against the pre-fix version of
agent_utils.py (3 failures matching exactly the Responses API cases)
and pass against the fix in 37087b7e1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 18:18:07 +08:00
theCyberTech
37087b7e1d fix(agent): recognize OpenAI Responses API tool-call shape in native tool loop
is_tool_call_list() and extract_tool_call_info() only recognized
Chat-Completions-style ({"function": {...}}), Anthropic-style
({"name", "input"}), and Gemini-style tool-call shapes. The Responses
API's function_call output items are flat dicts shaped
{"id", "name", "arguments"} with no nested "function" key and no
"input" key, so they matched none of the checks.

This caused is_tool_call_list() to misclassify a genuine tool call as
a plain text answer, so the native tool loop returned the raw
tool-call list as the agent's final output instead of executing the
tool. Even after recognizing the shape, extract_tool_call_info() would
have passed an empty arguments dict, since it only read "input" for
the dict fallback.

Verified against LLM(api="responses") with tools attached: the agent
now correctly executes the tool with the parsed arguments instead of
returning the unexecuted tool-call JSON as its answer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 18:12:20 +08:00
15 changed files with 331 additions and 512 deletions

View File

@@ -122,25 +122,11 @@ jobs:
.venv
key: uv-main-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
# Report the required check names (tests 3.103.13) when the matrix is skipped.
# Branch protection expects these names; a skipped matrix never reports them.
tests-skip:
name: tests (${{ matrix.python-version }})
needs: changes
if: needs.changes.outputs.code != 'true'
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13']
steps:
- name: Skip non-code change
run: echo "Non-code change, skipping tests"
# Summary job to provide single status for branch protection
tests:
name: tests
runs-on: ubuntu-latest
needs: [changes, tests-matrix, tests-skip]
needs: [changes, tests-matrix]
if: always()
steps:
- name: Check results

View File

@@ -76,25 +76,11 @@ jobs:
.venv
key: uv-main-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
# Report the required check names when the matrix is skipped.
# Branch protection expects these names; a skipped matrix never reports them.
type-checker-skip:
name: type-checker (${{ matrix.python-version }})
needs: changes
if: needs.changes.outputs.code != 'true'
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- name: Skip non-code change
run: echo "Non-code change, skipping type checks"
# Summary job to provide single status for branch protection
type-checker:
name: type-checker
runs-on: ubuntu-latest
needs: [changes, type-checker-matrix, type-checker-skip]
needs: [changes, type-checker-matrix]
if: always()
steps:
- name: Check results

View File

@@ -36,73 +36,24 @@ crewai [COMMAND] [OPTIONS] [ARGUMENTS]
### 1. Create
Create a new crew, flow, tool, skill, or template project.
Create a new crew or flow.
```shell Terminal
crewai create [OPTIONS] TYPE NAME
```
- `TYPE`: `crew`, `flow`, `tool`, `skill`, or `template`
- `NAME`: Name of the project, tool handle, skill, or template
- `TYPE`: Choose between "crew" or "flow"
- `NAME`: Name of the crew or flow
#### Crew
Example:
```shell Terminal
crewai create crew my_new_crew
crewai create crew my_new_crew --classic
crewai create flow my_new_flow
```
By default, `crewai create crew` creates a JSON-first crew project with `crew.jsonc` and `agents/*.jsonc`. Use `crewai create crew my_new_crew --classic` only when you want the older Python/YAML scaffold with `crew.py`, `config/agents.yaml`, and `config/tasks.yaml`.
#### Flow
```shell Terminal
crewai create flow my_new_flow
crewai create flow my_new_flow --declarative
```
#### Tool
Scaffold a custom tool repository:
```shell Terminal
crewai create tool my_tool
```
#### Skill
Scaffold an agent skill. Inside a crew project (where `pyproject.toml` exists), the skill is created under `./skills/`:
```shell Terminal
crewai create skill my-skill
crewai create skill my-skill --no-project
```
Use `--no-project` to create the skill in the current directory instead of `./skills/`.
#### Template
Add a remote project template to the current directory:
```shell Terminal
crewai create template my-template
crewai create template my-template --output-dir custom_dir
```
Use `--output-dir` to override the output folder name (defaults to the template name).
#### Deprecated create aliases
These older commands still work but print a yellow deprecation warning. Prefer the `crewai create <type>` forms above.
| Deprecated | Use instead |
| :--- | :--- |
| `crewai tool create <handle>` | `crewai create tool <handle>` |
| `crewai skill create <name>` | `crewai create skill <name>` |
| `crewai template add <name>` | `crewai create template <name>` |
Lifecycle commands are unchanged — for example `crewai tool install`, `crewai skill publish`, and `crewai template list` stay under their resource groups.
### 2. Version
Show the installed version of CrewAI.

View File

@@ -32,10 +32,10 @@ You often need **both**: skills for expertise, tools for action. They are config
The CLI is the supported way to create a skill — it scaffolds the directory layout and a valid `SKILL.md` for you:
```shell Terminal
crewai create skill code-review
crewai skill create code-review
```
Inside a crew project (where `pyproject.toml` lives) this creates `./skills/code-review/`; outside a project it creates `./code-review/` in the current directory (you can force that behavior with `--no-project` on `crewai create skill`):
Inside a crew project (where `pyproject.toml` lives) this creates `./skills/code-review/`; outside a project it creates `./code-review/` in the current directory (you can force that behavior with `--no-project`):
```
skills/
@@ -178,16 +178,12 @@ agent = Agent(
## Creating, Publishing, and Installing Skills
Skills have a full lifecycle managed by the CLI: **create them with `crewai create skill`, publish them with `crewai skill publish`** — hand-rolling directories works for local experiments, but the CLI is the intended workflow and keeps your skill layout and frontmatter valid.
<Note>
`crewai skill create` is deprecated and still works with a warning. Use `crewai create skill` instead.
</Note>
Skills have a full lifecycle managed by the CLI: **create them with `crewai skill create`, publish them with `crewai skill publish`** — hand-rolling directories works for local experiments, but the CLI is the intended workflow and keeps your skill layout and frontmatter valid.
### Create
```shell Terminal
crewai create skill my-skill
crewai skill create my-skill
```
Scaffolds the directory (into `./skills/` inside a crew project) with a template `SKILL.md`, plus empty `scripts/`, `references/`, and `assets/` directories. Edit `SKILL.md` to define the instructions.

View File

@@ -21,13 +21,9 @@ crewai create crew my_crew
crewai create flow my_flow
# Tool repository
crewai create tool my_tool
crewai tool create my_tool
```
<Note>
`crewai tool create` is deprecated and still works with a warning. Use `crewai create tool` instead.
</Note>
## Tool Setup: Point Assistants to AGENTS.md
### Codex

View File

@@ -19,7 +19,6 @@ from crewai_cli.utils import (
enable_prompt_line_editing,
is_dmn_mode_enabled,
read_toml,
warn_deprecated_command,
)
@@ -138,10 +137,7 @@ def uv(uv_args: tuple[str, ...]) -> None:
@crewai.command()
@click.argument(
"type",
required=False,
default=None,
type=click.Choice(["crew", "flow", "tool", "skill", "template"]),
"type", required=False, default=None, type=click.Choice(["crew", "flow"])
)
@click.argument("name", required=False, default=None)
@click.option("--provider", type=str, help="The provider to use for the crew")
@@ -156,21 +152,6 @@ def uv(uv_args: tuple[str, ...]) -> None:
is_flag=True,
help="Create a declarative Flow project instead of a Python Flow project",
)
@click.option(
"--no-project",
"in_project",
is_flag=True,
default=True,
flag_value=False,
help="Skill only: create in current dir instead of ./skills/",
)
@click.option(
"-o",
"--output-dir",
type=str,
default=None,
help="Template only: directory name for the template (defaults to template name)",
)
def create(
type: str | None,
name: str | None,
@@ -178,17 +159,14 @@ def create(
skip_provider: bool = False,
classic: bool = False,
declarative: bool = False,
in_project: bool = True,
output_dir: str | None = None,
) -> None:
"""Create a new crew, flow, tool, skill, or template."""
"""Create a new crew, or flow."""
dmn_mode = is_dmn_mode_enabled()
if not type:
if dmn_mode:
raise click.UsageError(
"TYPE is required when CREWAI_DMN is set. "
"Use `crewai create <type> <name>` where type is one of: "
"crew, flow, tool, skill, template."
"Use `crewai create crew <name>` or `crewai create flow <name>`."
)
from crewai_cli.tui_picker import pick
@@ -198,9 +176,6 @@ def create(
"flow",
"A deterministic workflow with full control over agents and crews",
),
("tool", "A custom tool for the CrewAI Tool Repository"),
("skill", "An agent skill with instructions and optional assets"),
("template", "A remote project template from the CrewAI gallery"),
]
type = pick("What would you like to create?", options)
if type is None:
@@ -214,36 +189,9 @@ def create(
click.style(f" Name of your {type}", fg="cyan", bold=True),
prompt_suffix=click.style(" ", fg="bright_white"), # noqa: RUF001
)
if dmn_mode and type == "crew":
if dmn_mode:
skip_provider = True
if not in_project and type != "skill":
raise click.UsageError("--no-project can only be used with skill projects.")
if output_dir is not None and type != "template":
raise click.UsageError("--output-dir can only be used with template projects.")
if type == "tool":
if declarative or classic or provider is not None or skip_provider:
raise click.UsageError(
"Crew and flow options cannot be used with tool projects."
)
from crewai_cli.tools.main import ToolCommand
ToolCommand().create(name)
elif type == "skill":
if declarative or classic or provider is not None or skip_provider:
raise click.UsageError(
"Crew and flow options cannot be used with skill projects."
)
from crewai_cli.skills.main import SkillCommand
SkillCommand().create(name, in_project=in_project)
elif type == "template":
if declarative or classic or provider is not None or skip_provider:
raise click.UsageError(
"Crew and flow options cannot be used with template projects."
)
template_cmd = TemplateCommand()
template_cmd.add_template(name, output_dir)
elif type == "crew":
if type == "crew":
if declarative:
raise click.UsageError("--declarative can only be used with flow projects")
if classic:
@@ -259,10 +207,7 @@ def create(
create_flow(name, declarative=declarative)
else:
click.secho(
"Error: Invalid type. Must be 'crew', 'flow', 'tool', 'skill', or 'template'.",
fg="red",
)
click.secho("Error: Invalid type. Must be 'crew' or 'flow'.", fg="red")
@crewai.command()
@@ -707,8 +652,6 @@ def tool() -> None:
@tool.command(name="create")
@click.argument("handle")
def tool_create(handle: str) -> None:
"""[Deprecated: use `crewai create tool`] Create a custom tool project."""
warn_deprecated_command(old="crewai tool create", new="crewai create tool")
from crewai_cli.tools.main import ToolCommand
tool_cmd = ToolCommand()
@@ -759,8 +702,6 @@ def skill() -> None:
help="Create skill in current dir instead of ./skills/",
)
def skill_create(name: str, in_project: bool) -> None:
"""[Deprecated: use `crewai create skill`] Create a new agent skill."""
warn_deprecated_command(old="crewai skill create", new="crewai create skill")
from crewai_cli.skills.main import SkillCommand
skill_cmd = SkillCommand()
@@ -824,8 +765,7 @@ def template_list() -> None:
help="Directory name for the template (defaults to template name)",
)
def template_add(name: str, output_dir: str | None) -> None:
"""[Deprecated: use `crewai create template`] Add a template to the current directory."""
warn_deprecated_command(old="crewai template add", new="crewai create template")
"""Add a template to the current directory."""
template_cmd = TemplateCommand()
template_cmd.add_template(name, output_dir)

View File

@@ -40,14 +40,6 @@ This ensures generated code always matches the version actually installed, not s
-`Agent(llm=ChatOpenAI(...))` → ✅ `Agent(llm="openai/gpt-4o")` or `Agent(llm=LLM(model="..."))`
- ❌ Passing raw OpenAI client objects → ✅ Use `crewai.LLM` wrapper
### Deprecated CLI scaffolding aliases (still supported)
These commands remain supported but print a yellow deprecation warning. Prefer the canonical forms:
- ⚠️ `crewai tool create <handle>` → ✅ `crewai create tool <handle>`
- ⚠️ `crewai skill create <name>` → ✅ `crewai create skill <name>`
- ⚠️ `crewai template add <name>` → ✅ `crewai create template <name>`
### How to verify you're using current patterns:
1. You ran the version check and docs lookup steps above before writing code
2. All LLM references use `crewai.LLM` or string shorthand (`"openai/gpt-4o"`)
@@ -145,26 +137,8 @@ uv sync # Sync dependencies
uv lock # Lock dependencies
# Project scaffolding
crewai create crew <name> --skip_provider # New crew project
crewai create flow <name> # New flow project
crewai create tool <handle> # Custom tool repository
crewai create skill <name> # Agent skill (./skills/ in crew projects)
crewai create skill <name> --no-project # Skill in current directory
crewai create template <name> # Remote project template
crewai create template <name> -o <output_dir> # Template with custom output directory
# Deprecated scaffolding aliases (still work; print a yellow warning)
# crewai tool create <handle> → crewai create tool <handle>
# crewai skill create <name> → crewai create skill <name>
# crewai template add <name> → crewai create template <name>
# Tool, skill, and template lifecycle (unchanged)
crewai tool install <handle>
crewai tool publish
crewai skill install @org/name
crewai skill publish
crewai skill list
crewai template list
crewai create crew <name> --skip_provider # New crew project
crewai create flow <name> --skip_provider # New flow project
# Running
crewai run # Run crew or flow (auto-detects from pyproject.toml)
@@ -653,26 +627,6 @@ class MyFlow(Flow):
| `@listen(method)` | Triggers when specified method completes. Receives output as argument |
| `@router(method)` | Conditional branching. Returns string labels that trigger `@listen("label")` |
### `@listen` labels vs handler names
The string in `@listen("...")` is an **event or route label**, not the Python method name. Router return values, route labels, and method completion events share one trigger namespace.
**Never** use the same name for the `@listen` label and the handler method:
```python
# ❌ Wrong — raises a validation error when the flow is instantiated
@listen("create_video")
def create_video(self):
...
# ✅ Correct — distinct handler name (handle_* prefix is a common pattern)
@listen("create_video")
def handle_create_video(self):
...
```
If validation were bypassed, matching names would also cause the handler to re-trigger itself in a loop at runtime. This applies to all flows; it is especially common in **conversational flows** (`conversational = True`), where `@listen("...")` is a router intent name.
### Structured State
```python
from pydantic import BaseModel
@@ -1159,9 +1113,7 @@ Python >=3.10, <3.14
```bash
uv tool install crewai # Install CrewAI CLI
uv tool list # Verify installation
crewai create crew my_crew --skip_provider # Scaffold a crew project
crewai create tool my_tool # Scaffold a tool repository
crewai create skill my_skill # Scaffold an agent skill
crewai create crew my_crew --skip_provider # Scaffold a new project
crewai install # Install project dependencies
crewai run # Execute
```
@@ -1196,4 +1148,3 @@ crewai run # Execute
- Using `process=Process.hierarchical` without setting `manager_llm` or `manager_agent`
- Circular delegation: set `allow_delegation=False` on specialist agents
- Not installing tools package: `uv add crewai-tools`
- **Matching `@listen("label")` to the handler method name** — raises a validation error at flow instantiation; would re-trigger in an infinite loop at runtime only if validation is bypassed. Use a different method name (e.g. `handle_create_video` for `@listen("create_video")`)

View File

@@ -44,19 +44,10 @@ __all__ = [
"render_template",
"tree_copy",
"tree_find_and_replace",
"warn_deprecated_command",
"write_env_file",
]
def warn_deprecated_command(*, old: str, new: str) -> None:
"""Print a yellow deprecation warning for a legacy CLI command path."""
click.secho(
f"Warning: The command '{old}' is deprecated. Use '{new}' instead.",
fg="yellow",
)
console = Console()
_TEMPLATE_TOKEN_RE = re.compile(r"{{([a-zA-Z_][a-zA-Z0-9_]*)}}")

View File

@@ -228,7 +228,6 @@ def test_create_requires_type_in_dmn_mode(runner):
assert result.exit_code == 2
assert "TYPE is required when CREWAI_DMN is set" in result.output
assert "crew, flow, tool, skill, template" in result.output
def test_create_requires_name_in_dmn_mode(runner):

View File

@@ -1,230 +0,0 @@
"""Tests for unified `crewai create <resource>` scaffolding commands."""
from unittest import mock
import pytest
from click.testing import CliRunner
from crewai_cli.cli import create, crewai
@pytest.fixture
def runner():
return CliRunner()
@mock.patch("crewai_cli.tools.main.ToolCommand")
def test_create_tool_invokes_tool_command(mock_tool_command_cls, runner):
result = runner.invoke(create, ["tool", "my_tool"])
assert result.exit_code == 0, result.output
mock_tool_command_cls.return_value.create.assert_called_once_with("my_tool")
assert "deprecated" not in result.output.lower()
@mock.patch("crewai_cli.tools.main.ToolCommand")
def test_tool_create_is_deprecated_and_still_works(mock_tool_command_cls, runner):
result = runner.invoke(crewai, ["tool", "create", "my_tool"])
assert result.exit_code == 0, result.output
mock_tool_command_cls.return_value.create.assert_called_once_with("my_tool")
assert (
"Warning: The command 'crewai tool create' is deprecated. "
"Use 'crewai create tool' instead."
in result.output
)
@mock.patch("crewai_cli.tools.main.ToolCommand")
@pytest.mark.parametrize("extra_args", [["--classic"], ["--declarative"], ["--provider", "openai"], ["--skip_provider"]])
def test_create_tool_rejects_crew_and_flow_flags(mock_tool_command_cls, runner, extra_args):
result = runner.invoke(create, ["tool", "my_tool", *extra_args])
assert result.exit_code == 2, result.output
assert "Crew and flow options cannot be used with tool projects." in result.output
mock_tool_command_cls.return_value.create.assert_not_called()
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_create_skill_invokes_skill_command(mock_skill_command_cls, runner):
result = runner.invoke(create, ["skill", "my-skill"])
assert result.exit_code == 0, result.output
mock_skill_command_cls.return_value.create.assert_called_once_with(
"my-skill", in_project=True
)
assert "deprecated" not in result.output.lower()
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_create_skill_no_project_flag(mock_skill_command_cls, runner):
result = runner.invoke(create, ["skill", "my-skill", "--no-project"])
assert result.exit_code == 0, result.output
mock_skill_command_cls.return_value.create.assert_called_once_with(
"my-skill", in_project=False
)
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_skill_create_is_deprecated_and_still_works(mock_skill_command_cls, runner):
result = runner.invoke(crewai, ["skill", "create", "my-skill"])
assert result.exit_code == 0, result.output
mock_skill_command_cls.return_value.create.assert_called_once_with(
"my-skill", in_project=True
)
assert (
"Warning: The command 'crewai skill create' is deprecated. "
"Use 'crewai create skill' instead."
in result.output
)
@mock.patch("crewai_cli.skills.main.SkillCommand")
@pytest.mark.parametrize(
"extra_args",
[["--classic"], ["--declarative"], ["--provider", "openai"], ["--skip_provider"]],
)
def test_create_skill_rejects_crew_and_flow_flags(
mock_skill_command_cls, runner, extra_args
):
result = runner.invoke(create, ["skill", "my-skill", *extra_args])
assert result.exit_code == 2, result.output
assert "Crew and flow options cannot be used with skill projects." in result.output
mock_skill_command_cls.return_value.create.assert_not_called()
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_create_crew_rejects_no_project_flag(mock_skill_command_cls, runner):
result = runner.invoke(create, ["crew", "my-crew", "--no-project"])
assert result.exit_code == 2, result.output
assert "--no-project can only be used with skill projects." in result.output
mock_skill_command_cls.return_value.create.assert_not_called()
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
def test_create_template_invokes_template_command(mock_template_command_cls, runner):
result = runner.invoke(create, ["template", "my-template"])
assert result.exit_code == 0, result.output
mock_template_command_cls.return_value.add_template.assert_called_once_with(
"my-template", None
)
assert "deprecated" not in result.output.lower()
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
def test_create_template_output_dir_flag(mock_template_command_cls, runner):
result = runner.invoke(
create, ["template", "my-template", "--output-dir", "custom_dir"]
)
assert result.exit_code == 0, result.output
mock_template_command_cls.return_value.add_template.assert_called_once_with(
"my-template", "custom_dir"
)
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
def test_template_add_is_deprecated_and_still_works(mock_template_command_cls, runner):
result = runner.invoke(
crewai, ["template", "add", "my-template", "--output-dir", "custom_dir"]
)
assert result.exit_code == 0, result.output
mock_template_command_cls.return_value.add_template.assert_called_once_with(
"my-template", "custom_dir"
)
assert (
"Warning: The command 'crewai template add' is deprecated. "
"Use 'crewai create template' instead."
in result.output
)
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
@pytest.mark.parametrize(
"extra_args",
[["--classic"], ["--declarative"], ["--provider", "openai"], ["--skip_provider"]],
)
def test_create_template_rejects_crew_and_flow_flags(
mock_template_command_cls, runner, extra_args
):
result = runner.invoke(create, ["template", "my-template", *extra_args])
assert result.exit_code == 2, result.output
assert (
"Crew and flow options cannot be used with template projects."
in result.output
)
mock_template_command_cls.return_value.add_template.assert_not_called()
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
def test_create_crew_rejects_output_dir_flag(mock_template_command_cls, runner):
result = runner.invoke(create, ["crew", "my-crew", "--output-dir", "custom_dir"])
assert result.exit_code == 2, result.output
assert "--output-dir can only be used with template projects." in result.output
mock_template_command_cls.return_value.add_template.assert_not_called()
@mock.patch("crewai_cli.cli.enable_prompt_line_editing")
@mock.patch("crewai_cli.cli.click.prompt", return_value="picked-tool")
@mock.patch("crewai_cli.tui_picker.pick", return_value="tool")
@mock.patch("crewai_cli.tools.main.ToolCommand")
def test_create_picker_supports_tool_skill_and_template(
mock_tool_command_cls,
mock_pick,
mock_prompt,
mock_enable_prompt,
runner,
):
result = runner.invoke(create, [])
assert result.exit_code == 0, result.output
mock_pick.assert_called_once()
picker_options = mock_pick.call_args[0][1]
assert {option[0] for option in picker_options} == {
"crew",
"flow",
"tool",
"skill",
"template",
}
mock_prompt.assert_called_once()
mock_tool_command_cls.return_value.create.assert_called_once_with("picked-tool")
_DMN_ENV = {"CREWAI_DMN": "True"}
@mock.patch("crewai_cli.tools.main.ToolCommand")
def test_create_tool_works_in_dmn_mode(mock_tool_command_cls, runner):
result = runner.invoke(create, ["tool", "my_tool"], env=_DMN_ENV)
assert result.exit_code == 0, result.output
mock_tool_command_cls.return_value.create.assert_called_once_with("my_tool")
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_create_skill_works_in_dmn_mode(mock_skill_command_cls, runner):
result = runner.invoke(create, ["skill", "my-skill"], env=_DMN_ENV)
assert result.exit_code == 0, result.output
mock_skill_command_cls.return_value.create.assert_called_once_with(
"my-skill", in_project=True
)
@mock.patch("crewai_cli.cli.TemplateCommand")
def test_create_template_works_in_dmn_mode(mock_template_command_cls, runner):
result = runner.invoke(create, ["template", "my-template"], env=_DMN_ENV)
assert result.exit_code == 0, result.output
mock_template_command_cls.return_value.add_template.assert_called_once_with(
"my-template", None
)

View File

@@ -747,7 +747,7 @@ class OpenAICompletion(BaseLLM):
)
@staticmethod
def _to_responses_input(message: LLMMessage) -> list[Any]:
def _to_responses_input(message: LLMMessage) -> list[dict[str, Any] | LLMMessage]:
"""Translate a chat-format message into Responses ``input`` items.
Tool calling is expressed differently by the two APIs. Chat Completions
@@ -765,18 +765,23 @@ class OpenAICompletion(BaseLLM):
role = message.get("role")
if role == "assistant" and message.get("tool_calls"):
items: list[Any] = []
items: list[dict[str, Any] | LLMMessage] = []
content = message.get("content")
if content:
items.append({"role": "assistant", "content": content})
for call in message["tool_calls"]:
function = call.get("function", {})
args = function.get("arguments")
if args is None or args == "":
args = "{}"
elif not isinstance(args, str):
args = json.dumps(args)
items.append(
{
"type": "function_call",
"call_id": call.get("id", ""),
"call_id": call.get("id") or f"call_{id(call)}",
"name": function.get("name", ""),
"arguments": function.get("arguments", "{}"),
"arguments": args,
}
)
return items
@@ -807,7 +812,7 @@ class OpenAICompletion(BaseLLM):
- Internally-tagged tool format (flat structure)
"""
instructions: str | None = self.instructions
input_messages: list[LLMMessage] = []
input_messages: list[dict[str, Any] | LLMMessage] = []
for message in messages:
if message.get("role") == "system":
@@ -821,7 +826,7 @@ class OpenAICompletion(BaseLLM):
input_messages.extend(self._to_responses_input(message))
# Prepend reasoning items for ZDR (zero-data-retention) chaining when configured
final_input: list[Any] = []
final_input: list[dict[str, Any] | LLMMessage] = []
if self.auto_chain_reasoning and self._last_reasoning_items:
final_input.extend(self._last_reasoning_items)
final_input.extend(input_messages if input_messages else messages)

View File

@@ -1404,9 +1404,9 @@ def is_tool_call_list(response: list[Any]) -> bool:
if isinstance(first_item, dict) and "name" in first_item and "input" in first_item:
return True
# OpenAI Responses API style: {"id", "name", "arguments"}, with no nested
# "function" object and no "input". Without this the list isn't recognized as
# tool calls, so the executor hands it back verbatim and the agent returns raw
# tool-call JSON instead of running the tool and producing a final answer.
# "function" object and no "input". This intentionally accepts the same broad
# shape as the Bedrock check above; only provider paths that return lists reach
# this classifier.
if (
isinstance(first_item, dict)
and "name" in first_item

View File

@@ -970,6 +970,170 @@ def test_openai_responses_api_with_system_message_extraction():
assert result.isupper() or "HELLO" in result.upper()
def test_openai_responses_api_converts_assistant_tool_calls_message():
"""Regression: assistant messages carrying tool_calls (Chat-Completions
shape) must become standalone function_call input items, since the
Responses API has no message shape for an assistant tool-call turn.
"""
llm = OpenAICompletion(model="gpt-4o-mini", api="responses")
messages = [
{"role": "user", "content": "Fetch https://example.com"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "fetch_page",
"arguments": '{"url": "https://example.com"}',
},
}
],
},
]
params = llm._prepare_responses_params(messages)
assert params["input"][0] == {"role": "user", "content": "Fetch https://example.com"}
assert params["input"][1] == {
"type": "function_call",
"call_id": "call_abc123",
"name": "fetch_page",
"arguments": '{"url": "https://example.com"}',
}
def test_openai_responses_api_preserves_assistant_content_with_tool_calls():
"""Assistant text must be retained when it accompanies tool calls."""
llm = OpenAICompletion(model="gpt-4o-mini", api="responses")
messages = [
{
"role": "assistant",
"content": "I'll fetch that page now.",
"tool_calls": [
{
"type": "function",
"id": "call_fetch_page",
"function": {
"name": "fetch_page",
"arguments": {"url": "https://example.com"},
},
}
],
}
]
params = llm._prepare_responses_params(messages)
assert params["input"][0] == {
"role": "assistant",
"content": "I'll fetch that page now.",
}
assert params["input"][1]["type"] == "function_call"
assert params["input"][1]["call_id"] == "call_fetch_page"
assert params["input"][1]["arguments"] == '{"url": "https://example.com"}'
def test_openai_responses_api_defaults_missing_tool_call_arguments():
"""Missing or empty tool-call arguments must become a valid JSON object."""
llm = OpenAICompletion(model="gpt-4o-mini", api="responses")
messages = [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_no_args",
"type": "function",
"function": {"name": "ping"},
},
{
"id": "call_empty_args",
"type": "function",
"function": {"name": "ping", "arguments": ""},
},
],
}
]
params = llm._prepare_responses_params(messages)
assert params["input"][0]["arguments"] == "{}"
assert params["input"][1]["arguments"] == "{}"
def test_openai_responses_api_converts_tool_result_message():
"""Regression: tool-role messages (Chat-Completions shape) must become
function_call_output input items for the Responses API.
"""
llm = OpenAICompletion(model="gpt-4o-mini", api="responses")
messages = [
{
"role": "tool",
"tool_call_id": "call_abc123",
"name": "fetch_page",
"content": "<html>page text</html>",
},
]
params = llm._prepare_responses_params(messages)
assert params["input"] == [
{
"type": "function_call_output",
"call_id": "call_abc123",
"output": "<html>page text</html>",
}
]
def test_openai_responses_api_multi_turn_tool_conversation_shape():
"""Regression: a full multi-turn tool-calling conversation (user ->
assistant tool_calls -> tool result) must convert entirely into valid
Responses API input items, with no leftover Chat-Completions-only keys
("tool_calls", "tool_call_id") that the Responses API would reject.
"""
llm = OpenAICompletion(model="gpt-4o-mini", api="responses")
messages = [
{"role": "user", "content": "Fetch https://example.com"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "fetch_page",
"arguments": '{"url": "https://example.com"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"name": "fetch_page",
"content": "<html>page text</html>",
},
]
params = llm._prepare_responses_params(messages)
for item in params["input"]:
assert "tool_calls" not in item
assert "tool_call_id" not in item
assert params["input"][1]["type"] == "function_call"
assert params["input"][2]["type"] == "function_call_output"
@pytest.mark.vcr()
def test_openai_responses_api_streaming():
"""Test Responses API with streaming enabled."""

View File

@@ -25,6 +25,8 @@ from crewai.utilities.agent_utils import (
_split_messages_into_chunks,
convert_tools_to_openai_schema,
execute_single_native_tool_call,
extract_tool_call_info,
is_tool_call_list,
NativeToolCallResult,
parse_tool_call_args,
summarize_messages,
@@ -981,6 +983,88 @@ class TestParallelSummarizationVCR:
assert "report.pdf" in summary_msg["files"]
class TestIsToolCallListResponsesApiShape:
"""Regression tests: OpenAI Responses API tool-call dicts must be recognized.
Responses API function_call output items are flat dicts shaped
{"id", "name", "arguments"} - no nested "function" key, and "arguments"
instead of Anthropic/Bedrock-style "input".
"""
def test_responses_api_dict_is_recognized_as_tool_call(self) -> None:
response = [
{
"id": "call_abc123",
"name": "fetch_page",
"arguments": '{"url": "https://example.com"}',
}
]
assert is_tool_call_list(response) is True
def test_plain_text_answer_not_misclassified(self) -> None:
assert is_tool_call_list(["just a string, not a tool call"]) is False
def test_empty_list_returns_false(self) -> None:
assert is_tool_call_list([]) is False
def test_chat_completions_style_still_recognized(self) -> None:
response = [{"function": {"name": "fetch_page", "arguments": "{}"}}]
assert is_tool_call_list(response) is True
def test_bedrock_anthropic_style_still_recognized(self) -> None:
response = [{"name": "fetch_page", "input": {"url": "https://example.com"}}]
assert is_tool_call_list(response) is True
class TestExtractToolCallInfoResponsesApiShape:
"""Regression tests: extract_tool_call_info must parse Responses API dicts."""
def test_responses_api_dict_extracts_real_arguments(self) -> None:
tool_call = {
"id": "call_abc123",
"name": "fetch_page",
"arguments": '{"url": "https://example.com"}',
}
result = extract_tool_call_info(tool_call)
assert result is not None
call_id, func_name, func_args = result
assert call_id == "call_abc123"
assert func_name == "fetch_page"
assert func_args == '{"url": "https://example.com"}'
def test_responses_api_dict_does_not_return_empty_args(self) -> None:
tool_call = {
"id": "call_xyz",
"name": "fetch_page",
"arguments": '{"url": "https://example.com"}',
}
_, _, func_args = extract_tool_call_info(tool_call)
assert func_args != {}
def test_bedrock_anthropic_style_still_uses_input(self) -> None:
tool_call = {"name": "fetch_page", "input": {"url": "https://example.com"}}
_, func_name, func_args = extract_tool_call_info(tool_call)
assert func_name == "fetch_page"
assert func_args == {"url": "https://example.com"}
def test_chat_completions_style_still_uses_nested_function(self) -> None:
tool_call = {
"id": "call_1",
"function": {"name": "fetch_page", "arguments": "{}"},
}
_, func_name, func_args = extract_tool_call_info(tool_call)
assert func_name == "fetch_page"
assert func_args == "{}"
def test_non_dict_unrecognized_shape_returns_none(self) -> None:
assert extract_tool_call_info("just a string") is None
def test_unrecognized_dict_shape_returns_empty_name_and_args(self) -> None:
call_id, func_name, func_args = extract_tool_call_info({"unrelated": "data"})
assert func_name == ""
assert func_args == {}
class TestParseToolCallArgs:
"""Unit tests for parse_tool_call_args."""

94
uv.lock generated
View File

@@ -1056,7 +1056,7 @@ name = "coloredlogs"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "humanfriendly", marker = "python_full_version < '3.11'" },
{ name = "humanfriendly" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
wheels = [
@@ -1154,7 +1154,7 @@ resolution-markers = [
"python_full_version < '3.11' and platform_machine == 's390x'",
]
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
wheels = [
@@ -1229,7 +1229,7 @@ resolution-markers = [
"python_full_version == '3.11.*' and platform_machine == 's390x'",
]
dependencies = [
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
wheels = [
@@ -1881,34 +1881,34 @@ wheels = [
[package.optional-dependencies]
cudart = [
{ name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cuda-runtime" },
]
cufft = [
{ name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cufft" },
]
cufile = [
{ name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
{ name = "nvidia-cufile" },
]
cupti = [
{ name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cuda-cupti" },
]
curand = [
{ name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-curand" },
]
cusolver = [
{ name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cusolver" },
]
cusparse = [
{ name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cusparse" },
]
nvjitlink = [
{ name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-nvjitlink" },
]
nvrtc = [
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cuda-nvrtc" },
]
nvtx = [
{ name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-nvtx" },
]
[[package]]
@@ -2433,7 +2433,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -3022,8 +3022,8 @@ name = "grpcio-health-checking"
version = "1.71.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "grpcio", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "protobuf", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "grpcio" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/53/86/20994347ef36b7626fb74539f13128100dd8b7eaac67efc063264e6cdc80/grpcio_health_checking-1.71.2.tar.gz", hash = "sha256:1c21ece88c641932f432b573ef504b20603bdf030ad4e1ec35dd7fdb4ea02637", size = 16770, upload-time = "2025-06-28T04:24:08.768Z" }
wheels = [
@@ -3227,7 +3227,7 @@ name = "humanfriendly"
version = "10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyreadline3", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
wheels = [
@@ -4464,10 +4464,10 @@ resolution-markers = [
"python_full_version < '3.11' and platform_machine == 's390x'",
]
dependencies = [
{ name = "jsonref", marker = "python_full_version < '3.12'" },
{ name = "mcp", extra = ["ws"], marker = "python_full_version < '3.12'" },
{ name = "pydantic", marker = "python_full_version < '3.12'" },
{ name = "python-dotenv", marker = "python_full_version < '3.12'" },
{ name = "jsonref" },
{ name = "mcp", extra = ["ws"] },
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/28/64fc666fa5d86bb1b048c167975d4ea19210f9f8571b64b26563739774ac/mcpadapt-0.1.19.tar.gz", hash = "sha256:dfab84fc75cc84a49a40bd61079773b1faf840227b74b82c71a7755b9c1957c5", size = 4227721, upload-time = "2025-10-16T07:11:56.736Z" }
wheels = [
@@ -4485,10 +4485,10 @@ resolution-markers = [
"python_full_version == '3.12.*' and platform_machine == 's390x'",
]
dependencies = [
{ name = "jsonref", marker = "python_full_version >= '3.12'" },
{ name = "mcp", extra = ["ws"], marker = "python_full_version >= '3.12'" },
{ name = "pydantic", marker = "python_full_version >= '3.12'" },
{ name = "python-dotenv", marker = "python_full_version >= '3.12'" },
{ name = "jsonref" },
{ name = "mcp", extra = ["ws"] },
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e3/71/1bbbe157e55d30ab4a74fa878f6942cc0586e9820f03e03451a3d2297e9b/mcpadapt-0.1.20.tar.gz", hash = "sha256:4047c0da61e481dd0673a48936a427da9e6547c6cf0d580ff4e4761dcf058ed1", size = 4203656, upload-time = "2025-10-24T15:35:02.135Z" }
wheels = [
@@ -5504,12 +5504,12 @@ name = "onnxruntime"
version = "1.23.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coloredlogs", marker = "python_full_version < '3.11'" },
{ name = "flatbuffers", marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "packaging", marker = "python_full_version < '3.11'" },
{ name = "protobuf", marker = "python_full_version < '3.11'" },
{ name = "sympy", marker = "python_full_version < '3.11'" },
{ name = "coloredlogs" },
{ name = "flatbuffers" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "sympy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" },
@@ -8157,7 +8157,7 @@ resolution-markers = [
"python_full_version < '3.11' and platform_machine == 's390x'",
]
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
wheels = [
@@ -8221,7 +8221,7 @@ resolution-markers = [
"python_full_version == '3.11.*' and platform_machine == 's390x'",
]
dependencies = [
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [
@@ -9852,13 +9852,13 @@ resolution-markers = [
"python_full_version < '3.11' and platform_machine == 's390x'",
]
dependencies = [
{ name = "authlib", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "deprecation", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "grpcio", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "grpcio-health-checking", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "httpx", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "pydantic", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "validators", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "authlib" },
{ name = "deprecation" },
{ name = "grpcio" },
{ name = "grpcio-health-checking" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "validators" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/b9/7b9e05cf923743aa1479afcd85c48ebca82d031c3c3a5d02b1b3fcb52eb9/weaviate_client-4.16.2.tar.gz", hash = "sha256:eb7107a3221a5ad68d604cafc65195bd925a9709512ea0b6fe0dd212b0678fab", size = 681321, upload-time = "2025-07-22T09:10:48.79Z" }
wheels = [
@@ -9877,13 +9877,13 @@ resolution-markers = [
"python_full_version == '3.11.*' and platform_machine == 's390x'",
]
dependencies = [
{ name = "authlib", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
{ name = "grpcio", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
{ name = "httpx", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
{ name = "packaging", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
{ name = "protobuf", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
{ name = "pydantic", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
{ name = "validators", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
{ name = "authlib" },
{ name = "grpcio" },
{ name = "httpx" },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "pydantic" },
{ name = "validators" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2b/b8/103f3aaa246d4e932f4cfeb846e51436966f2aeedf60c2665a3fc51a975a/weaviate_client-4.21.3.tar.gz", hash = "sha256:d7b1f2b0cecbc747e9427f4e3b9463cdfee090746bfbbd40e59cfa25ea2afd4a", size = 847895, upload-time = "2026-06-02T13:03:51.598Z" }
wheels = [