Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ env/
Thumbs.db
._*

# Canyon artifacts. `.car` is generated from the application source by the
# porting skill and `ventis build`; it is never committed.
.car/

# Generated stubs
stubs/
grpc_stubs/
Expand Down
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,26 @@ cd my-app
```
This command creates a new directory `my-app` with the following structure:
```
├── agents/ # Agent implementations and YAML definitions
│ ├── example_agent.py
│ └── example_agent.yaml
├── workflows/ # Workflow scripts (deployed as REST APIs)
│ └── example_workflow.py
├── config/
│ ├── global_controller.yaml # Deployment configuration
│ └── policy.yaml # Access control rules
├── stubs/ # Generated agent stubs (auto-generated)
├── grpc_stubs/ # Generated gRPC stubs (auto-generated)
└── README.md # Readme for the project
├── .car/
│ ├── app/ # Source copy used for builds
│ ├── config/
│ │ ├── global_controller.yaml
│ │ ├── example_agent.yaml # Agent declaration
│ │ └── policy.yaml
│ ├── stubs/
│ ├── grpc_stubs/
│ └── docker_container/
└── README.md
```
The Readme in the newly created project directory provides a quick overview of the project and how to use it. Including how to add new files etc. We provide some overview in next few steps.


#### Step 2: Define Your Agents
Place your agent logic (`.py`) and definitions (`.yaml`) in the `agents/` directory.
Agent declarations live under `.car/config/`. The source used for builds is
copied to `.car/app/` by `canyonos integrate`.

- **`agents/my_agent.yaml`**: Defines methods and schemas.
- **`agents/my_agent.py`**: Contains the actual Python implementation.
- **`.car/config/my_agent.yaml`**: Defines methods and schemas.
- **`.car/app/path/to/my_agent.py`**: Contains the agent implementation.

We have provided an example of a finance agent and a market research agent in the `examples/` directory. To run the example, copy files into your newly created project directory from within the your my-app directory with the command -

Expand All @@ -69,7 +69,7 @@ cp -r ../examples/* ./
## Deployment Guide

#### Step 1: Configure the Global Controller
Edit `config/global_controller.yaml` in your project directory to list the agents you want to deploy, their `provider`, `replicas`, and resource limits. Add a per-agent `requirements: [pkg, ...]` list for any extra pip packages that agent's code imports — only a small base list (grpc, redis, pyyaml, psutil, etc.) is installed by default.
Edit `.car/config/global_controller.yaml` in your project directory to list the agents you want to deploy, their `provider`, `replicas`, and resource limits. Add a per-agent `requirements: [pkg, ...]` list for any extra pip packages that agent's code imports — only a small base list (grpc, redis, pyyaml, psutil, etc.) is installed by default.

#### Step 1.1: Passing secrets to agents (optional)

Expand Down
4 changes: 2 additions & 2 deletions tests/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ cd "$TEST_DIR"
echo ">> 1. Generating new project..."
ventis new-project $PROJECT_NAME
cd $PROJECT_NAME
grep -v 'gpu:' config/global_controller.yaml > config/global_controller.yaml.tmp
mv config/global_controller.yaml.tmp config/global_controller.yaml
grep -v 'gpu:' .car/config/global_controller.yaml > .car/config/global_controller.yaml.tmp
mv .car/config/global_controller.yaml.tmp .car/config/global_controller.yaml

echo ">> 2. Building agents (ventis build)..."
ventis build
Expand Down
103 changes: 95 additions & 8 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,35 @@ def test_deploy_runs_ec2_preflight_for_ec2_config(
preflight.assert_called_once_with(config, os.getcwd())
controller.run.assert_called_once_with()

@patch("atexit.register")
@patch("signal.signal")
@patch("ventis.cli._ensure_grpc_stubs_importable")
@patch("ventis.cli._preflight_ec2_deploy")
def test_deploy_uses_car_when_present(
self, preflight, ensure_grpc, _signal_patch, _atexit_patch
):
controller = MagicMock()
controller_module = self._fake_controller_module(controller)
args = SimpleNamespace(config=".car/config/global_controller.yaml")

with tempfile.TemporaryDirectory() as tmpdir, patch(
"ventis.cli.os.path.isfile", return_value=True
), patch(
"ventis.cli._load_config", return_value={"agents": []}
), patch.dict(
sys.modules, {"ventis.controller.global_controller": controller_module}
):
Path(tmpdir, ".car").mkdir()
cwd = os.getcwd()
os.chdir(tmpdir)
try:
cli.cmd_deploy(args)
finally:
os.chdir(cwd)

ensure_grpc.assert_called_once_with(os.path.join(os.path.realpath(tmpdir), ".car"))
preflight.assert_not_called()

@patch("ventis.cli._ensure_grpc_stubs_importable")
@patch("ventis.cli._require_docker_for_ec2")
def test_preflight_does_not_require_ssh_fields(self, require_docker, ensure_grpc):
Expand All @@ -107,24 +136,40 @@ def _run_build(

Returns (docker_calls, generate_docker_mock, generate_workflow_docker_mock).
"""
config_path = project_dir / "config" / "global_controller.yaml"
artifact_root = (
project_dir / ".car" if (project_dir / ".car").is_dir() else project_dir
)
config_path = artifact_root / "config" / "global_controller.yaml"
args = SimpleNamespace(config=str(config_path))
docker_calls = []

def fake_run(cmd, check):
docker_calls.append(cmd)
return SimpleNamespace(returncode=0)

def fake_glob(pattern):
if pattern.endswith("*.proto"):
return ["proto/a.proto"]
if agent_yaml_paths:
self.assertEqual(
os.path.realpath(Path(pattern).parent),
os.path.realpath(Path(agent_yaml_paths[0]).parent),
)
return agent_yaml_paths

def fake_generate_stub(yaml_path, _output_path):
with open(yaml_path) as f:
self.assertIn("agent", yaml.safe_load(f))

with (
patch(
"ventis.cli._get_package_dir",
return_value=str(project_dir / "package"),
),
patch("ventis.cli.glob.glob", side_effect=fake_glob),
patch(
"ventis.cli.glob.glob",
side_effect=[agent_yaml_paths, ["proto/a.proto"]],
"ventis.stub_generator.generate_stub", side_effect=fake_generate_stub
),
patch("ventis.stub_generator.generate_stub"),
patch("ventis.stub_generator.generate_docker") as generate_docker,
patch(
"ventis.stub_generator.generate_workflow_docker"
Expand Down Expand Up @@ -240,6 +285,32 @@ def test_build_uses_buildx_bake_when_available(self):
)
self.assertEqual(targets["workflow"]["tags"], ["ventis-workflow"])

def test_build_uses_car_when_present(self):
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir)
artifact_root = project_dir / ".car"
source_root = artifact_root / "app"
source_root.mkdir(parents=True)
source_yaml = self._write_agent_and_workflow_config(source_root)
source_root.joinpath("config").rename(artifact_root / "config")
agent_yaml = artifact_root / "config" / source_yaml.name
source_yaml.rename(agent_yaml)

manifest = artifact_root / "config" / "global_controller.yaml"
_, generate_docker, generate_workflow_docker = self._run_build(
project_dir, [str(manifest), str(agent_yaml)], buildx_available=True
)

for call in (generate_docker, generate_workflow_docker):
self.assertEqual(
os.path.realpath(call.call_args.kwargs["project_dir"]),
os.path.realpath(source_root),
)
self.assertEqual(
os.path.realpath(generate_docker.call_args.kwargs["output_dir"]),
os.path.realpath(artifact_root / "docker_container" / "ExampleAgent"),
)

def test_build_with_no_agents_builds_nothing(self):
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir)
Expand All @@ -252,7 +323,7 @@ def test_build_with_no_agents_builds_nothing(self):

self.assertFalse(any(call[0] == "docker" for call in docker_calls))

def test_build_skips_agent_without_entrypoint(self):
def test_build_fails_when_stub_cannot_be_generated(self):
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir)
(project_dir / "config").mkdir()
Expand All @@ -268,9 +339,8 @@ def test_build_skips_agent_without_entrypoint(self):
)
)

docker_calls, _, _ = self._run_build(project_dir, [], buildx_available=True)

self.assertFalse(any(call[0] == "docker" for call in docker_calls))
with self.assertRaises(SystemExit):
self._run_build(project_dir, [], buildx_available=True)

def _write_requirements_config(self, project_dir):
"""Scaffold one plain agent, one agent with `requirements`, one workflow with `requirements`."""
Expand Down Expand Up @@ -373,5 +443,22 @@ def test_build_ignores_non_list_requirements(self):
self.assertEqual(generate_docker.call_args.kwargs["requirements"], [])


class CliCleanTests(unittest.TestCase):
def test_clean_uses_car_when_present(self):
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir)
(project_dir / ".car" / "stubs").mkdir(parents=True)
(project_dir / "stubs").mkdir()
cwd = os.getcwd()
os.chdir(project_dir)
try:
cli.cmd_clean(SimpleNamespace())
finally:
os.chdir(cwd)

self.assertFalse((project_dir / ".car" / "stubs").exists())
self.assertTrue((project_dir / "stubs").exists())


if __name__ == "__main__":
unittest.main()
Loading
Loading