Skip to content

Commit 8d99473

Browse files
committed
Version to 1.15.0. Small logging/formatting improvements.
1 parent 1defd73 commit 8d99473

File tree

7 files changed

+115
-37
lines changed

7 files changed

+115
-37
lines changed

dreadnode/agent/agent.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from copy import deepcopy
55

66
import rigging as rg
7+
from loguru import logger
78
from pydantic import ConfigDict, Field, PrivateAttr, SkipValidation, field_validator
89
from rigging.message import inject_system_content
910
from ulid import ULID # can't access via rg
@@ -237,6 +238,8 @@ async def _generate(
237238
try:
238239
messages = rg.caching.apply_cache_mode_to_messages(self.caching, [messages])[0]
239240

241+
logger.trace(f"Generating with model '{generator.model}'. Messages: {messages!r}")
242+
240243
generated = (await generator.generate_messages([messages], [params]))[0]
241244
if isinstance(generated, BaseException):
242245
raise generated # noqa: TRY301
@@ -252,6 +255,7 @@ async def _generate(
252255
)
253256

254257
except Exception as error: # noqa: BLE001
258+
logger.error(f"Error during generation: {error}", exc_info=True)
255259
chat = rg.Chat(
256260
messages,
257261
[],
@@ -278,6 +282,13 @@ async def _stream( # noqa: PLR0912, PLR0915
278282
stop_conditions = self.stop_conditions
279283
session_id = ULID()
280284

285+
logger.info(
286+
f"Starting Agent '{self.name}' ({session_id}): "
287+
f"model='{self.model}', "
288+
f"max_steps={self.max_steps}, "
289+
f"tools={[tool.name for tool in self.all_tools]}"
290+
)
291+
281292
# Event dispatcher
282293

283294
async def _dispatch(event: AgentEvent) -> t.AsyncIterator[AgentEvent]:
@@ -292,6 +303,11 @@ async def _dispatch(event: AgentEvent) -> t.AsyncIterator[AgentEvent]:
292303
if not applicable_hooks:
293304
return
294305

306+
logger.debug(
307+
f"Agent '{self.name}' ({session_id}) dispatching '{type(event).__name__}': "
308+
f"applicable_hooks={[get_callable_name(h, short=True) for h in applicable_hooks]}"
309+
)
310+
295311
# Run all applicable hooks and collect their reactions
296312
hook_reactions: dict[str, Reaction | None] = {}
297313
for hook in applicable_hooks:
@@ -310,6 +326,10 @@ async def _dispatch(event: AgentEvent) -> t.AsyncIterator[AgentEvent]:
310326
if reaction is None:
311327
continue
312328

329+
logger.debug(
330+
f"Agent '{self.name}' ({session_id}) hook '{hook_name}' returned reaction: {reaction!r}"
331+
)
332+
313333
if not isinstance(reaction, Reaction):
314334
warn_at_user_stacklevel(
315335
f"Hook '{hook_name}' returned {reaction}, but expected a Reaction.",
@@ -386,6 +406,9 @@ async def _dispatch(event: AgentEvent) -> t.AsyncIterator[AgentEvent]:
386406
return
387407

388408
if isinstance(winning_reaction, RetryWithFeedback):
409+
logger.debug(
410+
f"Agent '{self.name}' ({session_id}) injecting feedback for retry: '{winning_reaction.feedback}'"
411+
)
389412
messages.append(rg.Message("user", winning_reaction.feedback))
390413
raise Retry(messages=messages) from winning_reaction
391414

@@ -408,6 +431,10 @@ async def _process_tool_call(
408431
):
409432
yield event
410433

434+
logger.debug(
435+
f"Executing tool '{tool_call.name}' with args: {tool_call.function.arguments}"
436+
)
437+
411438
message: rg.Message
412439
stop = False
413440
tool = next((t for t in self.all_tools if t.name == tool_call.name), None)
@@ -431,6 +458,7 @@ async def _process_tool_call(
431458
yield event
432459
raise
433460
else:
461+
logger.warning(f"Tool '{tool_call.name}' not found.")
434462
message = rg.Message.from_model(
435463
rg.model.SystemErrorModel(content=f"Tool '{tool_call.name}' not found.")
436464
)
@@ -516,6 +544,7 @@ async def _process_tool_call(
516544
# Check for stop conditions
517545

518546
if any(cond(events) for cond in stop_conditions):
547+
logger.info("A stop condition was met. Ending run.")
519548
break
520549

521550
# Check if stalled
@@ -524,6 +553,10 @@ async def _process_tool_call(
524553
if not stop_conditions:
525554
break
526555

556+
logger.warning(
557+
f"Agent '{self.name}' ({session_id}) stalled: No tool calls and no stop conditions met."
558+
)
559+
527560
async for event in _dispatch(
528561
AgentStalled(
529562
session_id=session_id,
@@ -589,6 +622,23 @@ async def _process_tool_call(
589622
thread.messages = messages
590623
thread.events.extend(events)
591624

625+
total_usage = _total_usage_from_events(events)
626+
log_message = (
627+
f"Agent '{self.name}' finished: "
628+
f"reason='{stop_reason}', "
629+
f"steps={step - 1}, "
630+
f"total_tokens={total_usage.total_tokens}, "
631+
f"in_tokens={total_usage.input_tokens}, "
632+
f"out_tokens={total_usage.output_tokens}"
633+
)
634+
635+
if stop_reason == "finished":
636+
logger.success(log_message)
637+
elif stop_reason == "error":
638+
logger.error(f"{log_message}, error='{error!r}'")
639+
else:
640+
logger.warning(log_message)
641+
592642
yield AgentEnd(
593643
session_id=session_id,
594644
agent=self,

dreadnode/cli/agent/cli.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import inspect
23
import itertools
34
import typing as t
45
from inspect import isawaitable
@@ -55,7 +56,8 @@ async def run( # noqa: PLR0912, PLR0915
5556
agent: str,
5657
*tokens: t.Annotated[str, cyclopts.Parameter(show=False, allow_leading_hyphen=True)],
5758
config: Path | None = None,
58-
dreadnode_config: DreadnodeConfig | None = None,
59+
raw: t.Annotated[bool, cyclopts.Parameter(["-r", "--raw"], negative=False)] = False,
60+
dn_config: DreadnodeConfig | None = None,
5961
) -> None:
6062
"""
6163
Run an agent by name, file, or module.
@@ -70,6 +72,7 @@ async def run( # noqa: PLR0912, PLR0915
7072
Args:
7173
agent: The agent to run, e.g., 'my_agents.py:basic' or 'basic'.
7274
config: Optional path to a TOML/YAML/JSON configuration file for the agent.
75+
raw: If set, only display raw logging output without additional formatting.
7376
"""
7477
from dreadnode.agent import Agent
7578

@@ -107,38 +110,43 @@ async def run( # noqa: PLR0912, PLR0915
107110
agent_blueprint = agents_by_lower_name[agent_name.lower()]
108111

109112
config_model = get_config_model(agent_blueprint)
110-
config_parameter = cyclopts.Parameter(name="*", group="Agent Config")(config_model)
111-
112-
config_default = None
113+
config_annotation = cyclopts.Parameter(name="*", group="Agent Config")(config_model)
114+
config_default: t.Any = inspect.Parameter.empty
113115
with contextlib.suppress(Exception):
114116
config_default = config_model()
115-
config_parameter = config_parameter | None # type: ignore [assignment]
116117

117118
async def agent_cli(
118119
input: t.Annotated[str, cyclopts.Parameter(help="Input to the agent")],
119120
*,
120121
config: t.Any = config_default,
121-
dreadnode_config: DreadnodeConfig | None = dreadnode_config,
122+
dn_config: DreadnodeConfig | None = dn_config,
122123
) -> None:
123-
(dreadnode_config or DreadnodeConfig()).apply()
124-
124+
dn_config = dn_config or DreadnodeConfig()
125+
if raw and dn_config.log_level is None:
126+
dn_config.log_level = "info"
127+
dn_config.apply()
125128
agent = hydrate(agent_blueprint, config)
126-
flat_config = flatten_model(config)
127129

128130
rich.print(f"Running agent: [bold]{agent.name}[/bold] with config:")
129-
for key, value in flat_config.items():
131+
for key, value in flatten_model(config).items():
130132
rich.print(f" |- {key}: {value}")
131133
rich.print()
132134

133135
async with agent.stream(input) as stream:
134136
async for event in stream:
135137
rich.print(event)
136138

137-
agent_cli.__annotations__["config"] = config_parameter
139+
agent_cli.__annotations__["config"] = config_annotation
140+
141+
help_text = f"Run the '{agent_name}' agent."
142+
if agent_blueprint.__doc__:
143+
help_text += "\n\n" + agent_blueprint.__doc__
144+
if agent_blueprint.description:
145+
help_text += "\n\n" + agent_blueprint.description
138146

139147
agent_app = cyclopts.App(
140148
name=agent_name,
141-
help=f"Run the '{agent_name}' agent.",
149+
help=help_text,
142150
help_on_error=True,
143151
help_flags=("help"),
144152
version_flags=(),

dreadnode/cli/task/cli.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import inspect
23
import itertools
34
import typing as t
45
from inspect import isawaitable
@@ -54,7 +55,7 @@ async def run( # noqa: PLR0912, PLR0915
5455
task: str,
5556
*tokens: t.Annotated[str, cyclopts.Parameter(show=False, allow_leading_hyphen=True)],
5657
config: Path | None = None,
57-
dreadnode_config: DreadnodeConfig | None = None,
58+
dn_config: DreadnodeConfig | None = None,
5859
) -> None:
5960
"""
6061
Run a task by name, file, or module.
@@ -104,19 +105,17 @@ async def run( # noqa: PLR0912, PLR0915
104105
task_blueprint = tasks_by_lower_name[task_name.lower()]
105106

106107
config_model = get_config_model(task_blueprint)
107-
config_parameter = cyclopts.Parameter(name="*", group="Task Config")(config_model)
108-
109-
config_default = None
108+
config_annotation = cyclopts.Parameter(name="*", group="Task Config")(config_model)
109+
config_default: t.Any = inspect.Parameter.empty
110110
with contextlib.suppress(Exception):
111111
config_default = config_model()
112-
config_parameter = config_parameter | None # type: ignore[assignment]
113112

114113
async def task_cli(
115114
*,
116115
config: t.Any = config_default,
117-
dreadnode_config: DreadnodeConfig | None = dreadnode_config,
116+
dn_config: DreadnodeConfig | None = dn_config,
118117
) -> None:
119-
(dreadnode_config or DreadnodeConfig()).apply()
118+
(dn_config or DreadnodeConfig()).apply()
120119

121120
hydrated_task = hydrate(task_blueprint, config)
122121
flat_config = flatten_model(config)
@@ -128,11 +127,15 @@ async def task_cli(
128127

129128
await hydrated_task()
130129

131-
task_cli.__annotations__["config"] = config_parameter
130+
task_cli.__annotations__["config"] = config_annotation
131+
132+
help_text = f"Run the '{task_name}' task."
133+
if task_blueprint.__doc__:
134+
help_text += "\n\n" + task_blueprint.__doc__
132135

133136
task_app = cyclopts.App(
134137
name=task_name,
135-
help=task_blueprint.__doc__ or f"Run the '{task_name}' task.",
138+
help=help_text,
136139
help_on_error=True,
137140
help_flags=("help"),
138141
version_flags=(),

dreadnode/optimization/format.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def format_study(study: "Study") -> RenderableType:
4242
"""
4343
Format a single Study object into a detailed rich Panel.
4444
"""
45+
from dreadnode.airt import Attack
46+
4547
details = Table(
4648
box=box.MINIMAL,
4749
show_header=False,
@@ -51,7 +53,15 @@ def format_study(study: "Study") -> RenderableType:
5153
details.add_column("Value", style="white")
5254

5355
details.add_row(Text("Description", justify="right"), study.description or "-")
54-
details.add_row(Text("Task Factory", justify="right"), get_callable_name(study.task_factory))
56+
57+
# TODO(nick): Move attack formatting out of here
58+
if isinstance(study, Attack):
59+
details.add_row(Text("Target", justify="right"), repr(study.target))
60+
else:
61+
details.add_row(
62+
Text("Task Factory", justify="right"), get_callable_name(study.task_factory)
63+
)
64+
5565
details.add_row(Text("Search Strategy", justify="right"), study.search_strategy.name)
5666

5767
if study.dataset is not None:
@@ -60,10 +70,13 @@ def format_study(study: "Study") -> RenderableType:
6070
)
6171

6272
if study.objectives:
63-
objective_names = ", ".join(f"[cyan]{name}[/]" for name in study.objective_names)
64-
details.add_row(Text("Objectives", justify="right"), objective_names)
65-
directions = ", ".join(f"[yellow]{direction}[/]" for direction in study.directions)
66-
details.add_row(Text("Directions", justify="right"), directions)
73+
objectives = " | ".join(
74+
f"[cyan]{name} :arrow_upper_right:[/]"
75+
if direction == "maximize"
76+
else f"[magenta]{name} :arrow_lower_right:[/]"
77+
for name, direction in zip(study.objective_names, study.directions, strict=True)
78+
)
79+
details.add_row(Text("Objectives", justify="right"), objectives)
6780

6881
if study.constraints:
6982
constraint_names = ", ".join(

dreadnode/util.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,12 @@
3333
from loguru import logger
3434

3535
get_user_frame_and_stacklevel = _get_user_frame_and_stacklevel
36-
warn_at_user_stacklevel = _warn_at_user_stacklevel
3736
get_filepath_attribute = _get_filepath_attribute
3837
is_user_code = _is_user_code
3938

4039

4140
import dreadnode # noqa: E402
4241

43-
warn_at_user_stacklevel = _warn_at_user_stacklevel
44-
4542
SysExcInfo = (
4643
tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None]
4744
)
@@ -742,6 +739,18 @@ def _internal_error_exc_info() -> SysExcInfo:
742739
return original_exc_info
743740

744741

742+
def warn_at_user_stacklevel(msg: str, category: type[Warning]) -> None:
743+
"""
744+
Issue a warning at the user code stack level and log it.
745+
746+
Args:
747+
msg: The warning message.
748+
category: The warning category.
749+
"""
750+
logger.warning(msg)
751+
_warn_at_user_stacklevel(msg, category)
752+
753+
745754
@contextmanager
746755
def handle_internal_errors() -> t.Iterator[None]:
747756
"""

pyproject.toml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "dreadnode"
3-
version = "1.14.1"
3+
version = "1.15.0"
44
description = "Dreadnode SDK"
55
authors = [{ name = "Nick Landers", email = "monoxgas@gmail.com" }]
66
readme = "README.md"
@@ -44,7 +44,6 @@ scoring = [
4444
"presidio-analyzer>=2.2.359,<3.0.0",
4545
"scikit-learn>=1.7.1,<2.0.0",
4646
"confusables>=1.2.0,<2.0.0",
47-
4847
"nltk>=3.9.1,<4.0.0",
4948
"textblob>=0.19.0,<1.0.0",
5049
"textstat>=0.7.10,<1.0.0",
@@ -80,10 +79,6 @@ dn = "dreadnode.__main__:run"
8079
[tool.poetry.plugins."pipx.run"]
8180
dreadnode = 'dreadnode.__main__:run'
8281

83-
84-
[tool.poetry.group.dev.dependencies]
85-
86-
8782
[project.urls]
8883
Homepage = "https://github.com/dreadnode/sdk"
8984
Repository = "https://github.com/dreadnode/sdk"

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)