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.
Files changed (198) hide show
  1. {dreadnode-1.15.3 → dreadnode-1.16.0}/PKG-INFO +2 -2
  2. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/agent.py +136 -69
  3. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/events.py +2 -3
  4. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/result.py +4 -0
  5. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/api/client.py +149 -3
  6. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/api/models.py +79 -0
  7. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/main.py +4 -0
  8. dreadnode-1.16.0/dreadnode/cli/rbac/__init__.py +3 -0
  9. dreadnode-1.16.0/dreadnode/cli/rbac/organizations.py +29 -0
  10. dreadnode-1.16.0/dreadnode/cli/rbac/workspaces.py +151 -0
  11. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/shared.py +6 -0
  12. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/constants.py +11 -0
  13. dreadnode-1.16.0/dreadnode/exporter.py +25 -0
  14. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/main.py +353 -22
  15. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/task.py +9 -8
  16. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/tracing/span.py +24 -10
  17. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/util.py +17 -0
  18. {dreadnode-1.15.3 → dreadnode-1.16.0}/pyproject.toml +5 -4
  19. dreadnode-1.16.0/tests/test_agent.py +629 -0
  20. dreadnode-1.16.0/tests/test_task_output_linking.py +141 -0
  21. {dreadnode-1.15.3 → dreadnode-1.16.0}/.gitignore +0 -0
  22. {dreadnode-1.15.3 → dreadnode-1.16.0}/LICENSE +0 -0
  23. {dreadnode-1.15.3 → dreadnode-1.16.0}/README.md +0 -0
  24. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/__init__.py +0 -0
  25. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/__main__.py +0 -0
  26. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/__init__.py +0 -0
  27. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/error.py +0 -0
  28. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/format.py +0 -0
  29. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/__init__.py +0 -0
  30. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/backoff.py +0 -0
  31. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/base.py +0 -0
  32. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/metrics.py +0 -0
  33. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/hooks/summarize.py +0 -0
  34. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/prompts/__init__.py +0 -0
  35. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/prompts/summarize.py +0 -0
  36. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/reactions.py +0 -0
  37. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/stop.py +0 -0
  38. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/thread.py +0 -0
  39. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/__init__.py +0 -0
  40. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/base.py +0 -0
  41. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/execute.py +0 -0
  42. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/fs.py +0 -0
  43. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/memory.py +0 -0
  44. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/planning.py +0 -0
  45. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/reporting.py +0 -0
  46. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/agent/tools/tasking.py +0 -0
  47. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/__init__.py +0 -0
  48. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/__init__.py +0 -0
  49. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/base.py +0 -0
  50. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/goat.py +0 -0
  51. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/hop_skip_jump.py +0 -0
  52. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/nes.py +0 -0
  53. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/prompt.py +0 -0
  54. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/simba.py +0 -0
  55. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/tap.py +0 -0
  56. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/attack/zoo.py +0 -0
  57. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/__init__.py +0 -0
  58. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/hop_skip_jump.py +0 -0
  59. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/image_utils.py +0 -0
  60. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/nes.py +0 -0
  61. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/simba.py +0 -0
  62. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/search/zoo.py +0 -0
  63. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/target/__init__.py +0 -0
  64. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/target/base.py +0 -0
  65. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/target/custom.py +0 -0
  66. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/airt/target/llm.py +0 -0
  67. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/api/__init__.py +0 -0
  68. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/api/util.py +0 -0
  69. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/__init__.py +0 -0
  70. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/credential_manager.py +0 -0
  71. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/merger.py +0 -0
  72. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/storage.py +0 -0
  73. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/artifact/tree_builder.py +0 -0
  74. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/__init__.py +0 -0
  75. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/agent/__init__.py +0 -0
  76. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/agent/cli.py +0 -0
  77. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/api.py +0 -0
  78. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/attack/__init__.py +0 -0
  79. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/attack/cli.py +0 -0
  80. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/docker.py +0 -0
  81. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/eval/__init__.py +0 -0
  82. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/eval/cli.py +0 -0
  83. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/github.py +0 -0
  84. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/__init__.py +0 -0
  85. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/cli.py +0 -0
  86. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/compose.py +0 -0
  87. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/constants.py +0 -0
  88. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/download.py +0 -0
  89. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/env_mgmt.py +0 -0
  90. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/tag.py +0 -0
  91. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/platform/version.py +0 -0
  92. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/profile/__init__.py +0 -0
  93. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/profile/cli.py +0 -0
  94. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/study/__init__.py +0 -0
  95. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/study/cli.py +0 -0
  96. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/task/__init__.py +0 -0
  97. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/cli/task/cli.py +0 -0
  98. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/common_types.py +0 -0
  99. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/convert.py +0 -0
  100. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/__init__.py +0 -0
  101. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/audio.py +0 -0
  102. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/base.py +0 -0
  103. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/image.py +0 -0
  104. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/object_3d.py +0 -0
  105. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/table.py +0 -0
  106. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/text.py +0 -0
  107. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/data_types/video.py +0 -0
  108. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/discovery.py +0 -0
  109. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/error.py +0 -0
  110. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/__init__.py +0 -0
  111. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/console.py +0 -0
  112. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/dataset.py +0 -0
  113. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/eval.py +0 -0
  114. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/events.py +0 -0
  115. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/format.py +0 -0
  116. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/result.py +0 -0
  117. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/eval/sample.py +0 -0
  118. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/format.py +0 -0
  119. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/integrations/__init__.py +0 -0
  120. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/integrations/transformers.py +0 -0
  121. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/logging_.py +0 -0
  122. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/__init__.py +0 -0
  123. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/config.py +0 -0
  124. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/context.py +0 -0
  125. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/hydrate.py +0 -0
  126. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/meta/introspect.py +0 -0
  127. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/metric.py +0 -0
  128. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/object.py +0 -0
  129. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/__init__.py +0 -0
  130. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/collectors.py +0 -0
  131. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/console.py +0 -0
  132. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/events.py +0 -0
  133. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/format.py +0 -0
  134. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/result.py +0 -0
  135. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/sampling.py +0 -0
  136. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/__init__.py +0 -0
  137. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/base.py +0 -0
  138. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/boundary.py +0 -0
  139. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/graph.py +0 -0
  140. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/optuna_.py +0 -0
  141. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/search/random.py +0 -0
  142. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/stop.py +0 -0
  143. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/study.py +0 -0
  144. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/optimization/trial.py +0 -0
  145. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/py.typed +0 -0
  146. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/__init__.py +0 -0
  147. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/base.py +0 -0
  148. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/classification.py +0 -0
  149. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/consistency.py +0 -0
  150. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/contains.py +0 -0
  151. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/crucible.py +0 -0
  152. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/format.py +0 -0
  153. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/harm.py +0 -0
  154. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/image.py +0 -0
  155. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/json.py +0 -0
  156. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/judge.py +0 -0
  157. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/length.py +0 -0
  158. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/lexical.py +0 -0
  159. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/pii.py +0 -0
  160. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/readability.py +0 -0
  161. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/rigging.py +0 -0
  162. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/sentiment.py +0 -0
  163. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/similarity.py +0 -0
  164. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/scorers/util.py +0 -0
  165. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/serialization.py +0 -0
  166. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/tracing/__init__.py +0 -0
  167. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/tracing/constants.py +0 -0
  168. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/tracing/exporters.py +0 -0
  169. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/__init__.py +0 -0
  170. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/base.py +0 -0
  171. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/cipher.py +0 -0
  172. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/encoding.py +0 -0
  173. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/image.py +0 -0
  174. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/perturbation.py +0 -0
  175. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/refine.py +0 -0
  176. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/stylistic.py +0 -0
  177. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/substitution.py +0 -0
  178. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/swap.py +0 -0
  179. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/transforms/text.py +0 -0
  180. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/user_config.py +0 -0
  181. {dreadnode-1.15.3 → dreadnode-1.16.0}/dreadnode/version.py +0 -0
  182. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/airt/beam_search.ipynb +0 -0
  183. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/airt/graph_of_attacks_with_pruning.ipynb +0 -0
  184. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/airt/tap_vs_goat_eval.ipynb +0 -0
  185. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/airt/tree_of_attacks_with_pruning.ipynb +0 -0
  186. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/data_export.ipynb +0 -0
  187. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_artifact.ipynb +0 -0
  188. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/audio.ipynb +0 -0
  189. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/image.ipynb +0 -0
  190. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/object3d.ipynb +0 -0
  191. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/table.ipynb +0 -0
  192. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/log_object/video.ipynb +0 -0
  193. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/model_training.ipynb +0 -0
  194. {dreadnode-1.15.3 → dreadnode-1.16.0}/examples/rigging.ipynb +0 -0
  195. {dreadnode-1.15.3 → dreadnode-1.16.0}/tests/cli/test_config.py +0 -0
  196. {dreadnode-1.15.3 → dreadnode-1.16.0}/tests/cli/test_docker.py +0 -0
  197. {dreadnode-1.15.3 → dreadnode-1.16.0}/tests/cli/test_github.py +0 -0
  198. {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.15.3
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<4.0.0,>=3.2.1
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"applicable_hooks={[get_callable_name(h, short=True) for h in applicable_hooks]}"
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 applicable_hooks:
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
- yield reacted_event
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 = 1
568
+ step = 0
574
569
  error: Exception | str | None = None
575
570
 
576
- while step <= self.max_steps + 1:
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=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
- raise step_chat.error
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
- continue
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 > self.max_steps + 1:
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
- yield AgentEnd(
724
- session_id=session_id,
725
- agent=self,
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
- usage=_total_usage_from_events(events),
734
- steps=step,
735
- failed=stop_reason != "finished",
736
- error=error,
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", min(0, event.result.steps - 1))
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, hooks, commit=commit)) as stream:
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=min(0, last_event.result.steps - 1),
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, _: t.Any) -> None:
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.append(never())
920
- self.hooks.insert(
921
- 0,
922
- retry_with_feedback(
923
- event_type=AgentStalled,
924
- feedback="Continue the task if possible, use the 'finish_task' tool to complete it, or 'give_up_on_task' if it cannot be completed.",
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, project: str) -> Project:
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
- project (str): The project identifier.
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("GET", f"/strikes/projects/{project!s}")
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}")