dreadnode 1.15.3__tar.gz → 1.16.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dreadnode-1.15.3 → dreadnode-1.16.0}/PKG-INFO +2 -2
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/agent.py +136 -69
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/events.py +2 -3
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/result.py +4 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/api/client.py +149 -3
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/api/models.py +79 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/main.py +4 -0
- dreadnode-1.16.0/dreadnode/cli/rbac/__init__.py +3 -0
- dreadnode-1.16.0/dreadnode/cli/rbac/organizations.py +29 -0
- dreadnode-1.16.0/dreadnode/cli/rbac/workspaces.py +151 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/shared.py +6 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/constants.py +11 -0
- dreadnode-1.16.0/dreadnode/exporter.py +25 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/main.py +353 -22
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/task.py +9 -8
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/tracing/span.py +24 -10
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/util.py +17 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/pyproject.toml +5 -4
- dreadnode-1.16.0/tests/test_agent.py +629 -0
- dreadnode-1.16.0/tests/test_task_output_linking.py +141 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/.gitignore +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/LICENSE +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/README.md +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/__main__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/error.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/format.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/backoff.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/base.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/metrics.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/summarize.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/prompts/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/prompts/summarize.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/reactions.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/stop.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/thread.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/base.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/execute.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/fs.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/memory.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/planning.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/reporting.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/tasking.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/base.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/goat.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/hop_skip_jump.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/nes.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/prompt.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/simba.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/tap.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/zoo.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/hop_skip_jump.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/image_utils.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/nes.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/simba.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/zoo.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/target/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/target/base.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/target/custom.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/target/llm.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/api/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/api/util.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/credential_manager.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/merger.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/storage.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/tree_builder.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/agent/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/agent/cli.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/api.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/attack/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/attack/cli.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/docker.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/eval/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/eval/cli.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/github.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/cli.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/compose.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/constants.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/download.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/env_mgmt.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/tag.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/version.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/profile/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/profile/cli.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/study/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/study/cli.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/task/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/task/cli.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/common_types.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/convert.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/audio.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/base.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/image.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/object_3d.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/table.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/text.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/video.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/discovery.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/error.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/console.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/dataset.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/eval.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/events.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/format.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/result.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/sample.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/format.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/integrations/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/integrations/transformers.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/logging_.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/config.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/context.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/hydrate.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/introspect.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/metric.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/object.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/collectors.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/console.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/events.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/format.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/result.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/sampling.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/base.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/boundary.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/graph.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/optuna_.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/random.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/stop.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/study.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/trial.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/py.typed +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/base.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/classification.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/consistency.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/contains.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/crucible.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/format.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/harm.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/image.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/json.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/judge.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/length.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/lexical.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/pii.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/readability.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/rigging.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/sentiment.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/similarity.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/util.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/serialization.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/tracing/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/tracing/constants.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/tracing/exporters.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/__init__.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/base.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/cipher.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/encoding.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/image.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/perturbation.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/refine.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/stylistic.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/substitution.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/swap.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/text.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/user_config.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/version.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/airt/beam_search.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/airt/graph_of_attacks_with_pruning.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/airt/tap_vs_goat_eval.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/airt/tree_of_attacks_with_pruning.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/data_export.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_artifact.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/audio.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/image.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/object3d.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/table.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/video.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/model_training.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/rigging.ipynb +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/tests/cli/test_config.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/tests/cli/test_docker.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/tests/cli/test_github.py +0 -0
- {dreadnode-1.15.3 → dreadnode-1.16.0}/tests/test_meta.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dreadnode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
4
4
|
Summary: Dreadnode SDK
|
|
5
5
|
Project-URL: Homepage, https://github.com/dreadnode/sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/dreadnode/sdk
|
|
@@ -222,7 +222,7 @@ Requires-Dist: pydantic<3.0.0,>=2.9.2
|
|
|
222
222
|
Requires-Dist: python-jsonpath>=2.0.1
|
|
223
223
|
Requires-Dist: python-ulid<4.0.0,>=3.0.0
|
|
224
224
|
Requires-Dist: pyyaml>=6.0.2
|
|
225
|
-
Requires-Dist: rigging
|
|
225
|
+
Requires-Dist: rigging>=3.3.4
|
|
226
226
|
Requires-Dist: universal-pathlib<0.4.0,>=0.3.3
|
|
227
227
|
Provides-Extra: all
|
|
228
228
|
Requires-Dist: confusables<2.0.0,>=1.2.0; extra == 'all'
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
2
4
|
import typing as t
|
|
3
5
|
from contextlib import aclosing, asynccontextmanager
|
|
4
6
|
from copy import deepcopy
|
|
@@ -20,7 +22,6 @@ from dreadnode.agent.events import (
|
|
|
20
22
|
AgentEventInStep,
|
|
21
23
|
AgentStalled,
|
|
22
24
|
AgentStart,
|
|
23
|
-
AgentStopReason,
|
|
24
25
|
GenerationEnd,
|
|
25
26
|
Reacted,
|
|
26
27
|
StepStart,
|
|
@@ -37,7 +38,7 @@ from dreadnode.agent.reactions import (
|
|
|
37
38
|
Retry,
|
|
38
39
|
RetryWithFeedback,
|
|
39
40
|
)
|
|
40
|
-
from dreadnode.agent.result import AgentResult
|
|
41
|
+
from dreadnode.agent.result import AgentResult, AgentStopReason
|
|
41
42
|
from dreadnode.agent.stop import StopCondition, never
|
|
42
43
|
from dreadnode.agent.thread import Thread
|
|
43
44
|
from dreadnode.agent.tools import AnyTool, Tool, Toolset, discover_tools_on_obj
|
|
@@ -59,7 +60,6 @@ from dreadnode.util import (
|
|
|
59
60
|
litellm.suppress_debug_info = True
|
|
60
61
|
|
|
61
62
|
CommitBehavior = t.Literal["always", "on-success"]
|
|
62
|
-
HookMap = dict[type[AgentEvent], list[Hook]]
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
class AgentWarning(UserWarning):
|
|
@@ -250,6 +250,9 @@ class Agent(Model):
|
|
|
250
250
|
new.scorers = scorers if scorers is not None else new.scorers
|
|
251
251
|
new.assert_scores = assert_scores if assert_scores is not None else new.assert_scores
|
|
252
252
|
|
|
253
|
+
# Retrigger model_post_init functions to ensure consistency
|
|
254
|
+
new.model_post_init(None)
|
|
255
|
+
|
|
253
256
|
return new
|
|
254
257
|
|
|
255
258
|
def _get_transforms(self) -> list[rg.Transform]:
|
|
@@ -269,29 +272,11 @@ class Agent(Model):
|
|
|
269
272
|
transforms.append(rg.transform.tools_to_json_with_tag_transform)
|
|
270
273
|
case "json":
|
|
271
274
|
transforms.append(rg.transform.tools_to_json_transform)
|
|
275
|
+
case "pythonic":
|
|
276
|
+
transforms.append(rg.transform.tools_to_pythonic_transform)
|
|
272
277
|
|
|
273
278
|
return transforms
|
|
274
279
|
|
|
275
|
-
def _get_hooks(self) -> dict[type[AgentEvent], list[Hook]]:
|
|
276
|
-
hooks: dict[type[AgentEvent], list[Hook]] = {}
|
|
277
|
-
for hook in self.hooks:
|
|
278
|
-
sig = inspect.signature(hook)
|
|
279
|
-
if not (params := list(sig.parameters.values())):
|
|
280
|
-
continue
|
|
281
|
-
event_type = params[0].annotation
|
|
282
|
-
|
|
283
|
-
if hasattr(event_type, "__origin__") and event_type.__origin__ is t.Union:
|
|
284
|
-
union_args = event_type.__args__
|
|
285
|
-
for arg in union_args:
|
|
286
|
-
if inspect.isclass(arg) and issubclass(arg, AgentEvent):
|
|
287
|
-
hooks.setdefault(arg, []).append(hook)
|
|
288
|
-
elif inspect.isclass(event_type) and issubclass(event_type, AgentEvent):
|
|
289
|
-
hooks.setdefault(event_type, []).append(hook)
|
|
290
|
-
else:
|
|
291
|
-
hooks.setdefault(AgentEvent, []).append(hook)
|
|
292
|
-
|
|
293
|
-
return hooks
|
|
294
|
-
|
|
295
280
|
async def _generate(
|
|
296
281
|
self,
|
|
297
282
|
messages: list[rg.Message],
|
|
@@ -352,7 +337,6 @@ class Agent(Model):
|
|
|
352
337
|
self,
|
|
353
338
|
thread: "Thread",
|
|
354
339
|
messages: list[rg.Message],
|
|
355
|
-
hooks: HookMap,
|
|
356
340
|
*,
|
|
357
341
|
commit: CommitBehavior,
|
|
358
342
|
) -> t.AsyncGenerator[AgentEvent, None]:
|
|
@@ -369,26 +353,21 @@ class Agent(Model):
|
|
|
369
353
|
|
|
370
354
|
# Event dispatcher
|
|
371
355
|
|
|
372
|
-
async def _dispatch(event: AgentEvent) -> t.AsyncIterator[AgentEvent]:
|
|
356
|
+
async def _dispatch(event: AgentEvent) -> t.AsyncIterator[AgentEvent]: # noqa: PLR0912
|
|
373
357
|
nonlocal messages, events
|
|
374
358
|
|
|
375
359
|
yield event
|
|
376
360
|
|
|
377
361
|
events.append(event)
|
|
378
362
|
|
|
379
|
-
# If we have no hooks, just return the event
|
|
380
|
-
applicable_hooks = list(set(hooks.get(type(event), []) + hooks.get(AgentEvent, [])))
|
|
381
|
-
if not applicable_hooks:
|
|
382
|
-
return
|
|
383
|
-
|
|
384
363
|
logger.debug(
|
|
385
364
|
f"Agent '{self.name}' ({session_id}) dispatching '{type(event).__name__}': "
|
|
386
|
-
f"
|
|
365
|
+
f"hooks={[get_callable_name(h, short=True) for h in self.hooks]}"
|
|
387
366
|
)
|
|
388
367
|
|
|
389
368
|
# Run all applicable hooks and collect their reactions
|
|
390
369
|
hook_reactions: dict[str, Reaction | None] = {}
|
|
391
|
-
for hook in
|
|
370
|
+
for hook in self.hooks:
|
|
392
371
|
hook_name = getattr(
|
|
393
372
|
hook, "__name__", getattr(hook, "__qualname__", safe_repr(hook))
|
|
394
373
|
)
|
|
@@ -415,6 +394,20 @@ class Agent(Model):
|
|
|
415
394
|
)
|
|
416
395
|
continue
|
|
417
396
|
|
|
397
|
+
if isinstance(event, AgentEnd):
|
|
398
|
+
warn_at_user_stacklevel(
|
|
399
|
+
f"Hook '{hook_name}' returned {reaction} during AgentEnd, but reactions are ignored at this stage.",
|
|
400
|
+
AgentWarning,
|
|
401
|
+
)
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
if isinstance(event, Reacted):
|
|
405
|
+
warn_at_user_stacklevel(
|
|
406
|
+
f"Hook '{hook_name}' returned {reaction} during Reacted, but reactions are ignored at this stage.",
|
|
407
|
+
AgentWarning,
|
|
408
|
+
)
|
|
409
|
+
continue
|
|
410
|
+
|
|
418
411
|
hook_reactions[hook_name] = reaction
|
|
419
412
|
|
|
420
413
|
if not hook_reactions:
|
|
@@ -477,7 +470,9 @@ class Agent(Model):
|
|
|
477
470
|
reaction=winning_reaction,
|
|
478
471
|
)
|
|
479
472
|
events.append(reacted_event)
|
|
480
|
-
|
|
473
|
+
|
|
474
|
+
async for _event in _dispatch(reacted_event):
|
|
475
|
+
yield _event
|
|
481
476
|
|
|
482
477
|
if isinstance(winning_reaction, Continue):
|
|
483
478
|
messages = winning_reaction.messages
|
|
@@ -570,10 +565,12 @@ class Agent(Model):
|
|
|
570
565
|
|
|
571
566
|
# Core step loop
|
|
572
567
|
|
|
573
|
-
step =
|
|
568
|
+
step = 0
|
|
574
569
|
error: Exception | str | None = None
|
|
575
570
|
|
|
576
|
-
while step
|
|
571
|
+
while step < self.max_steps:
|
|
572
|
+
step += 1
|
|
573
|
+
|
|
577
574
|
try:
|
|
578
575
|
async for event in _dispatch(
|
|
579
576
|
StepStart(
|
|
@@ -589,7 +586,7 @@ class Agent(Model):
|
|
|
589
586
|
|
|
590
587
|
# Generation
|
|
591
588
|
|
|
592
|
-
step_chat = await self._generate(messages
|
|
589
|
+
step_chat = await self._generate(messages)
|
|
593
590
|
if step_chat.failed and step_chat.error:
|
|
594
591
|
async for event in _dispatch(
|
|
595
592
|
AgentError(
|
|
@@ -602,7 +599,9 @@ class Agent(Model):
|
|
|
602
599
|
)
|
|
603
600
|
):
|
|
604
601
|
yield event
|
|
605
|
-
|
|
602
|
+
|
|
603
|
+
error = t.cast("Exception", step_chat.error) # Should be Exception in rigging
|
|
604
|
+
break
|
|
606
605
|
|
|
607
606
|
# Sync extra fields to metadata for storage
|
|
608
607
|
step_chat.generated[-1].metadata.update(step_chat.extra)
|
|
@@ -649,7 +648,8 @@ class Agent(Model):
|
|
|
649
648
|
):
|
|
650
649
|
yield event
|
|
651
650
|
|
|
652
|
-
|
|
651
|
+
# If the agent is stalled and nobody handled it, break out
|
|
652
|
+
break
|
|
653
653
|
|
|
654
654
|
# Process tool calls
|
|
655
655
|
|
|
@@ -675,8 +675,6 @@ class Agent(Model):
|
|
|
675
675
|
if any(cond(events) for cond in stop_conditions):
|
|
676
676
|
break
|
|
677
677
|
|
|
678
|
-
step += 1
|
|
679
|
-
|
|
680
678
|
except Retry as e:
|
|
681
679
|
messages = e.messages or messages
|
|
682
680
|
continue
|
|
@@ -689,7 +687,7 @@ class Agent(Model):
|
|
|
689
687
|
break
|
|
690
688
|
|
|
691
689
|
stop_reason: AgentStopReason = "finished"
|
|
692
|
-
if step
|
|
690
|
+
if step >= self.max_steps:
|
|
693
691
|
error = MaxStepsError(max_steps=self.max_steps)
|
|
694
692
|
stop_reason = "max_steps_reached"
|
|
695
693
|
elif error is not None:
|
|
@@ -720,28 +718,32 @@ class Agent(Model):
|
|
|
720
718
|
else:
|
|
721
719
|
logger.warning(log_message)
|
|
722
720
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
thread=thread,
|
|
727
|
-
messages=messages,
|
|
728
|
-
events=events,
|
|
729
|
-
stop_reason=stop_reason,
|
|
730
|
-
result=AgentResult(
|
|
721
|
+
async for event in _dispatch(
|
|
722
|
+
AgentEnd(
|
|
723
|
+
session_id=session_id,
|
|
731
724
|
agent=self,
|
|
725
|
+
thread=thread,
|
|
732
726
|
messages=messages,
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
727
|
+
events=events,
|
|
728
|
+
stop_reason=stop_reason,
|
|
729
|
+
result=AgentResult(
|
|
730
|
+
agent=self,
|
|
731
|
+
messages=messages,
|
|
732
|
+
stop_reason=stop_reason,
|
|
733
|
+
usage=_total_usage_from_events(events),
|
|
734
|
+
steps=step,
|
|
735
|
+
failed=stop_reason != "finished",
|
|
736
|
+
error=error,
|
|
737
|
+
),
|
|
738
|
+
)
|
|
739
|
+
):
|
|
740
|
+
yield event
|
|
739
741
|
|
|
740
742
|
def _log_event_metrics(self, event: AgentEvent) -> None:
|
|
741
743
|
from dreadnode import log_metric
|
|
742
744
|
|
|
743
745
|
if isinstance(event, AgentEnd):
|
|
744
|
-
log_metric("steps_taken",
|
|
746
|
+
log_metric("steps_taken", max(0, event.result.steps - 1))
|
|
745
747
|
log_metric(f"stop_{event.stop_reason}", 1)
|
|
746
748
|
|
|
747
749
|
if not isinstance(event, AgentEventInStep):
|
|
@@ -778,7 +780,6 @@ class Agent(Model):
|
|
|
778
780
|
) -> t.AsyncGenerator[AgentEvent, None]:
|
|
779
781
|
from dreadnode import log_output, log_outputs, score, task_and_run
|
|
780
782
|
|
|
781
|
-
hooks = self._get_hooks()
|
|
782
783
|
messages = [*deepcopy(thread.messages), rg.Message("user", str(user_input))]
|
|
783
784
|
|
|
784
785
|
configuration = get_config_model(self)()
|
|
@@ -822,7 +823,7 @@ class Agent(Model):
|
|
|
822
823
|
params=trace_params,
|
|
823
824
|
):
|
|
824
825
|
try:
|
|
825
|
-
async with aclosing(self._stream(thread, messages,
|
|
826
|
+
async with aclosing(self._stream(thread, messages, commit=commit)) as stream:
|
|
826
827
|
async for event in stream:
|
|
827
828
|
last_event = event
|
|
828
829
|
self._log_event_metrics(event)
|
|
@@ -837,7 +838,7 @@ class Agent(Model):
|
|
|
837
838
|
if isinstance(last_event, AgentEnd):
|
|
838
839
|
log_outputs(
|
|
839
840
|
to="both",
|
|
840
|
-
steps_taken=
|
|
841
|
+
steps_taken=max(0, last_event.result.steps - 1),
|
|
841
842
|
reason=last_event.stop_reason,
|
|
842
843
|
failed=last_event.result.failed,
|
|
843
844
|
)
|
|
@@ -897,7 +898,7 @@ class Agent(Model):
|
|
|
897
898
|
|
|
898
899
|
class TaskAgent(Agent):
|
|
899
900
|
"""
|
|
900
|
-
A specialized agent for running tasks with a focus on completion and reporting.
|
|
901
|
+
A specialized agent mixin for running tasks with a focus on completion and reporting.
|
|
901
902
|
It extends the base Agent class to provide task-specific functionality.
|
|
902
903
|
|
|
903
904
|
- Automatically includes the `finish_task`, `give_up_on_task`, and `update_todo` tools.
|
|
@@ -905,7 +906,12 @@ class TaskAgent(Agent):
|
|
|
905
906
|
- Uses the `AgentStalled` event to handle stalled tasks by pushing the model to continue or finish the task.
|
|
906
907
|
"""
|
|
907
908
|
|
|
908
|
-
def model_post_init(self,
|
|
909
|
+
def model_post_init(self, context: t.Any) -> None:
|
|
910
|
+
super().model_post_init(context)
|
|
911
|
+
|
|
912
|
+
# TODO(nick): Would be better to have a pattern here for
|
|
913
|
+
# add-if-missing for tools, hooks, and stop conditions
|
|
914
|
+
|
|
909
915
|
if not any(tool for tool in self.tools if tool.name == "finish_task"):
|
|
910
916
|
self.tools.append(finish_task)
|
|
911
917
|
|
|
@@ -916,11 +922,72 @@ class TaskAgent(Agent):
|
|
|
916
922
|
self.tools.append(update_todo)
|
|
917
923
|
|
|
918
924
|
# Force the agent to use finish_task
|
|
919
|
-
self.stop_conditions.
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
)
|
|
926
|
-
)
|
|
925
|
+
if not any(cond for cond in self.stop_conditions if cond.name == "stop_never"):
|
|
926
|
+
self.stop_conditions.append(never())
|
|
927
|
+
|
|
928
|
+
if not any(
|
|
929
|
+
hook
|
|
930
|
+
for hook in self.hooks
|
|
931
|
+
if get_callable_name(hook, short=True) == "retry_with_feedback"
|
|
932
|
+
):
|
|
933
|
+
self.hooks.append(
|
|
934
|
+
retry_with_feedback(
|
|
935
|
+
event_type=AgentStalled,
|
|
936
|
+
feedback="No tool calls were observed. Continue the task if possible, use the 'finish_task' tool to complete it, or 'give_up_on_task' if it cannot be completed.",
|
|
937
|
+
)
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
class RegexRefAgent(Agent):
|
|
942
|
+
"""
|
|
943
|
+
An agent mixin that allows for dynamic references of prior text using regex patterns in tool arguments.
|
|
944
|
+
This helps prevent repeating large amounts of prior text in tool calls.
|
|
945
|
+
|
|
946
|
+
Instructions are automatically added to the agent's instructions to guide usage of the {find:<pattern>} syntax
|
|
947
|
+
along with a hook that resolves these references during tool calls.
|
|
948
|
+
"""
|
|
949
|
+
|
|
950
|
+
@staticmethod
|
|
951
|
+
async def resolve_regex_ref(event: AgentEvent) -> Reaction | None:
|
|
952
|
+
if not isinstance(event, ToolStart):
|
|
953
|
+
return None
|
|
954
|
+
|
|
955
|
+
for m in re.finditer(r"\{find:(.*?)\}", event.tool_call.arguments):
|
|
956
|
+
regex = m.group(1).replace("\\\\", "\\") # models tend to over-escape
|
|
957
|
+
logger.info(f"Found find reference: {regex}")
|
|
958
|
+
all_message_content = "\n\n".join([m.content for m in event.messages])
|
|
959
|
+
reference_matches = re.findall(regex, all_message_content)
|
|
960
|
+
if reference_matches:
|
|
961
|
+
logger.debug(f"Replacing '{m.group(0)}' with '{reference_matches[-1][:50]}...'.")
|
|
962
|
+
event.tool_call.function.arguments = event.tool_call.arguments.replace(
|
|
963
|
+
m.group(0), json.dumps(reference_matches[-1]).strip('"')
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
return None
|
|
967
|
+
|
|
968
|
+
def model_post_init(self, context: t.Any) -> None:
|
|
969
|
+
super().model_post_init(context)
|
|
970
|
+
|
|
971
|
+
if not any(
|
|
972
|
+
hook
|
|
973
|
+
for hook in self.hooks
|
|
974
|
+
if get_callable_name(hook, short=True) == "resolve_regex_ref"
|
|
975
|
+
):
|
|
976
|
+
self.hooks.append(RegexRefAgent.resolve_regex_ref)
|
|
977
|
+
|
|
978
|
+
instruction_section = dedent("""
|
|
979
|
+
# Regex Find Instructions
|
|
980
|
+
To efficiently reuse data from the conversation, you can pass {find:<pattern>} anywhere in tool arguments to dynamically
|
|
981
|
+
refer to prior text using a regex pattern. This helps prevent costly repetition of prior text.
|
|
982
|
+
|
|
983
|
+
You must escape special characters in the regex.
|
|
984
|
+
|
|
985
|
+
Example: If the history contains `$krb5tgs$23$*user...<long_hash>`, use:
|
|
986
|
+
`hashcat(hashes=["{find:\\$krb5tgs\\$.*}"], wordlist="...")`
|
|
987
|
+
and the system will find the full hash for you and insert it into the tool call.
|
|
988
|
+
""")
|
|
989
|
+
|
|
990
|
+
if self.instructions is None:
|
|
991
|
+
self.instructions = instruction_section
|
|
992
|
+
elif self.instructions and instruction_section not in self.instructions:
|
|
993
|
+
self.instructions += "\n\n" + instruction_section
|
|
@@ -24,13 +24,12 @@ from dreadnode.util import format_dict, shorten_string
|
|
|
24
24
|
if t.TYPE_CHECKING:
|
|
25
25
|
from dreadnode.agent.agent import Agent
|
|
26
26
|
from dreadnode.agent.reactions import Reaction
|
|
27
|
-
from dreadnode.agent.result import AgentResult
|
|
27
|
+
from dreadnode.agent.result import AgentResult, AgentStopReason
|
|
28
28
|
from dreadnode.agent.thread import Thread
|
|
29
29
|
from dreadnode.common_types import AnyDict
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
AgentEventT = t.TypeVar("AgentEventT", bound="AgentEvent")
|
|
33
|
-
AgentStopReason = t.Literal["finished", "max_steps_reached", "error", "stalled"]
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
@dataclass
|
|
@@ -292,7 +291,7 @@ class Reacted(AgentEventInStep):
|
|
|
292
291
|
|
|
293
292
|
@dataclass
|
|
294
293
|
class AgentEnd(AgentEvent):
|
|
295
|
-
stop_reason: AgentStopReason
|
|
294
|
+
stop_reason: "AgentStopReason"
|
|
296
295
|
result: "AgentResult"
|
|
297
296
|
|
|
298
297
|
def format_as_panel(self, *, truncate: bool = False) -> Panel: # noqa: ARG002
|
|
@@ -8,10 +8,13 @@ from rigging.message import Message
|
|
|
8
8
|
if t.TYPE_CHECKING:
|
|
9
9
|
from dreadnode.agent.agent import Agent
|
|
10
10
|
|
|
11
|
+
AgentStopReason = t.Literal["finished", "max_steps_reached", "error", "stalled"]
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
|
|
13
15
|
class AgentResult:
|
|
14
16
|
agent: "Agent"
|
|
17
|
+
stop_reason: AgentStopReason
|
|
15
18
|
messages: list[Message]
|
|
16
19
|
usage: Usage
|
|
17
20
|
steps: int
|
|
@@ -23,6 +26,7 @@ class AgentResult:
|
|
|
23
26
|
f"agent={self.agent.name}",
|
|
24
27
|
f"messages={len(self.messages)}",
|
|
25
28
|
f"usage='{self.usage}'",
|
|
29
|
+
f"stop_reason='{self.stop_reason}'",
|
|
26
30
|
f"steps={self.steps}",
|
|
27
31
|
]
|
|
28
32
|
|
|
@@ -6,6 +6,7 @@ import typing as t
|
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from urllib.parse import urlparse
|
|
9
|
+
from uuid import UUID
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
from loguru import logger
|
|
@@ -19,6 +20,8 @@ from dreadnode.api.models import (
|
|
|
19
20
|
ExportFormat,
|
|
20
21
|
GithubTokenResponse,
|
|
21
22
|
MetricAggregationType,
|
|
23
|
+
Organization,
|
|
24
|
+
PaginatedWorkspaces,
|
|
22
25
|
Project,
|
|
23
26
|
RawRun,
|
|
24
27
|
RawTask,
|
|
@@ -34,6 +37,8 @@ from dreadnode.api.models import (
|
|
|
34
37
|
TraceTree,
|
|
35
38
|
UserDataCredentials,
|
|
36
39
|
UserResponse,
|
|
40
|
+
Workspace,
|
|
41
|
+
WorkspaceFilter,
|
|
37
42
|
)
|
|
38
43
|
from dreadnode.api.util import (
|
|
39
44
|
convert_flat_tasks_to_tree,
|
|
@@ -276,16 +281,47 @@ class ApiClient:
|
|
|
276
281
|
response = self.request("GET", "/strikes/projects")
|
|
277
282
|
return [Project(**project) for project in response.json()]
|
|
278
283
|
|
|
279
|
-
def get_project(self,
|
|
284
|
+
def get_project(self, project_identifier: str | UUID, workspace_id: UUID) -> Project:
|
|
280
285
|
"""Retrieves details of a specific project.
|
|
281
286
|
|
|
282
287
|
Args:
|
|
283
|
-
|
|
288
|
+
project_identifier (str | UUID): The project identifier. ID, name, or slug.
|
|
284
289
|
|
|
285
290
|
Returns:
|
|
286
291
|
Project: The Project object.
|
|
287
292
|
"""
|
|
288
|
-
response = self.request(
|
|
293
|
+
response = self.request(
|
|
294
|
+
"GET",
|
|
295
|
+
f"/strikes/projects/{project_identifier!s}",
|
|
296
|
+
params={"workspace_id": workspace_id},
|
|
297
|
+
)
|
|
298
|
+
return Project(**response.json())
|
|
299
|
+
|
|
300
|
+
def create_project(
|
|
301
|
+
self,
|
|
302
|
+
name: str | UUID | None = None,
|
|
303
|
+
workspace_id: UUID | None = None,
|
|
304
|
+
organization_id: UUID | None = None,
|
|
305
|
+
) -> Project:
|
|
306
|
+
"""Creates a new project.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
name: The name of the project. If None, a default name will be used.
|
|
310
|
+
workspace_id: The workspace ID to create the project in. If None, the default workspace will be used.
|
|
311
|
+
organization_id: The organization ID to create the project in. If None, the default organization will be used.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Project: The created Project object.
|
|
315
|
+
"""
|
|
316
|
+
payload: dict[str, t.Any] = {}
|
|
317
|
+
if name is not None:
|
|
318
|
+
payload["name"] = name
|
|
319
|
+
if workspace_id is not None:
|
|
320
|
+
payload["workspace_id"] = str(workspace_id)
|
|
321
|
+
if organization_id is not None:
|
|
322
|
+
payload["org_id"] = str(organization_id)
|
|
323
|
+
|
|
324
|
+
response = self.request("POST", "/strikes/projects", json_data=payload)
|
|
289
325
|
return Project(**response.json())
|
|
290
326
|
|
|
291
327
|
def list_runs(self, project: str) -> list[RunSummary]:
|
|
@@ -757,3 +793,113 @@ class ApiClient:
|
|
|
757
793
|
response = self.request("GET", "/platform/templates/all", params=params)
|
|
758
794
|
zip_content: bytes = response.content
|
|
759
795
|
return zip_content
|
|
796
|
+
|
|
797
|
+
# RBAC
|
|
798
|
+
def list_organizations(self) -> list[Organization]:
|
|
799
|
+
"""
|
|
800
|
+
Retrieves a list of organizations the user belongs to.
|
|
801
|
+
|
|
802
|
+
Returns:
|
|
803
|
+
A list of organization names.
|
|
804
|
+
"""
|
|
805
|
+
response = self.request("GET", "/organizations")
|
|
806
|
+
return [Organization(**org) for org in response.json()]
|
|
807
|
+
|
|
808
|
+
def get_organization(self, org_id_or_key: UUID | str) -> Organization:
|
|
809
|
+
"""
|
|
810
|
+
Retrieves details of a specific organization.
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
org_id_or_key (str | UUID): The organization identifier.
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
Organization: The Organization object.
|
|
817
|
+
"""
|
|
818
|
+
response = self.request("GET", f"/organizations/{org_id_or_key!s}")
|
|
819
|
+
return Organization(**response.json())
|
|
820
|
+
|
|
821
|
+
def list_workspaces(self, filters: WorkspaceFilter | None = None) -> list[Workspace]:
|
|
822
|
+
"""
|
|
823
|
+
Retrieves a list of workspaces the user has access to.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
A list of workspace names.
|
|
827
|
+
"""
|
|
828
|
+
response = self.request(
|
|
829
|
+
"GET", "/workspaces", params=filters.model_dump() if filters else None
|
|
830
|
+
)
|
|
831
|
+
paginated_workspaces = PaginatedWorkspaces(**response.json())
|
|
832
|
+
# handle the pagination
|
|
833
|
+
all_workspaces: list[Workspace] = paginated_workspaces.workspaces.copy()
|
|
834
|
+
while paginated_workspaces.has_next:
|
|
835
|
+
response = self.request(
|
|
836
|
+
"GET",
|
|
837
|
+
"/workspaces",
|
|
838
|
+
params={
|
|
839
|
+
"page": paginated_workspaces.page + 1,
|
|
840
|
+
"limit": paginated_workspaces.limit,
|
|
841
|
+
**(filters.model_dump() if filters else {}),
|
|
842
|
+
},
|
|
843
|
+
)
|
|
844
|
+
next_page = PaginatedWorkspaces(**response.json())
|
|
845
|
+
all_workspaces.extend(next_page.workspaces)
|
|
846
|
+
paginated_workspaces.page = next_page.page
|
|
847
|
+
paginated_workspaces.has_next = next_page.has_next
|
|
848
|
+
|
|
849
|
+
return all_workspaces
|
|
850
|
+
|
|
851
|
+
def get_workspace(
|
|
852
|
+
self, workspace_id_or_key: UUID | str, org_id: UUID | None = None
|
|
853
|
+
) -> Workspace:
|
|
854
|
+
"""
|
|
855
|
+
Retrieves details of a specific workspace.
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
workspace_id_or_key (str | UUID): The workspace identifier.
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
Workspace: The Workspace object.
|
|
862
|
+
"""
|
|
863
|
+
params: dict[str, str] = {}
|
|
864
|
+
if org_id:
|
|
865
|
+
params = {"org_id": str(org_id)}
|
|
866
|
+
response = self.request("GET", f"/workspaces/{workspace_id_or_key!s}", params=params)
|
|
867
|
+
return Workspace(**response.json())
|
|
868
|
+
|
|
869
|
+
def create_workspace(
|
|
870
|
+
self,
|
|
871
|
+
name: str,
|
|
872
|
+
key: str,
|
|
873
|
+
organization_id: UUID,
|
|
874
|
+
description: str | None = None,
|
|
875
|
+
) -> Workspace:
|
|
876
|
+
"""
|
|
877
|
+
Creates a new workspace.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
name (str): The name of the workspace.
|
|
881
|
+
organization_id (str | UUID): The organization ID to create the workspace in.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Workspace: The created Workspace object.
|
|
885
|
+
"""
|
|
886
|
+
|
|
887
|
+
payload = {
|
|
888
|
+
"name": name,
|
|
889
|
+
"key": key,
|
|
890
|
+
"description": description,
|
|
891
|
+
"org_id": str(organization_id),
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
response = self.request("POST", "/workspaces", json_data=payload)
|
|
895
|
+
return Workspace(**response.json())
|
|
896
|
+
|
|
897
|
+
def delete_workspace(self, workspace_id: str | UUID) -> None:
|
|
898
|
+
"""
|
|
899
|
+
Deletes a specific workspace.
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
workspace_id (str | UUID): The workspace key.
|
|
903
|
+
"""
|
|
904
|
+
|
|
905
|
+
self.request("DELETE", f"/workspaces/{workspace_id!s}")
|