Skip to content

Commit ec3d598

Browse files
committed
Added utils file for LLM agent
1 parent 74bd10e commit ec3d598

2 files changed

Lines changed: 112 additions & 2 deletions

File tree

agents/attackers/llm_react/llm_agent_react_tools.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131
AgentStatus,
3232
GameState,
3333
)
34-
from utils import filter_log_files_from_state
35-
from reference.action_list_base_agent import RunStats
34+
from utils import RunStats, filter_log_files_from_state
3635

3736
try:
3837
from openai import OpenAI
@@ -256,6 +255,7 @@ class EpisodeOutcome:
256255
input_tokens: int
257256
output_tokens: int
258257
end_reason: str
258+
total_reward: float = 0.0
259259
transcript: list = field(default_factory=list)
260260

261261

@@ -623,6 +623,7 @@ def run_episode(
623623
last_result: Optional[str] = None
624624
end_reason = "unknown"
625625
step = 0
626+
total_reward = 0.0
626627
if observation:
627628
self._prev_state_sig = self._state_signature(observation.state)
628629

@@ -693,6 +694,8 @@ def run_episode(
693694

694695
observation = self.make_step(action)
695696
observation = filter_log_files_from_state(observation)
697+
if observation and observation.reward is not None:
698+
total_reward += float(observation.reward)
696699
last_result = (
697700
f"{action.type.name} executed. reward={observation.reward}. "
698701
"See updated state below."
@@ -748,6 +751,7 @@ def run_episode(
748751
input_tokens=self._input_tokens,
749752
output_tokens=self._output_tokens,
750753
end_reason=end_reason,
754+
total_reward=total_reward,
751755
transcript=transcript,
752756
)
753757

@@ -849,6 +853,7 @@ def main():
849853
)
850854
print(
851855
f" steps: {outcome.steps}, "
856+
f"reward: {outcome.total_reward:.2f}, "
852857
f"tokens: {total_tok:,} "
853858
f"(in={outcome.input_tokens:,} out={outcome.output_tokens:,})"
854859
)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from dataclasses import dataclass, field
2+
from typing import Protocol, runtime_checkable
3+
4+
from netsecgame import Observation
5+
6+
7+
def filter_log_files_from_state(observation: Observation) -> Observation:
8+
"""Removes verbose 'logfile' entities from the observation state."""
9+
for host in list(observation.state.known_data.keys()):
10+
data_list = observation.state.known_data[host]
11+
filtered_data = [data for data in data_list if data.id != "logfile"]
12+
if len(filtered_data) > 0:
13+
observation.state.known_data[host] = filtered_data
14+
else:
15+
del observation.state.known_data[host]
16+
return observation
17+
18+
19+
@runtime_checkable
20+
class HasEpisodeStats(Protocol):
21+
input_tokens: int
22+
output_tokens: int
23+
won: bool
24+
steps: int
25+
total_reward: float
26+
27+
28+
@dataclass
29+
class RunStats:
30+
"""Accumulates per-episode token, step, reward, and outcome statistics across a run."""
31+
32+
_wins: int = field(default=0, init=False)
33+
_episodes: int = field(default=0, init=False)
34+
_input_tokens: list = field(default_factory=list, init=False)
35+
_output_tokens: list = field(default_factory=list, init=False)
36+
_steps: list = field(default_factory=list, init=False)
37+
_rewards: list = field(default_factory=list, init=False)
38+
39+
def record(
40+
self,
41+
won: bool,
42+
input_tokens: int,
43+
output_tokens: int,
44+
steps: int = 0,
45+
total_reward: float = 0.0,
46+
) -> None:
47+
self._episodes += 1
48+
if won:
49+
self._wins += 1
50+
self._input_tokens.append(input_tokens)
51+
self._output_tokens.append(output_tokens)
52+
self._steps.append(steps)
53+
self._rewards.append(total_reward)
54+
55+
def record_outcome(self, outcome: "HasEpisodeStats") -> None:
56+
self.record(
57+
won=outcome.won,
58+
input_tokens=outcome.input_tokens,
59+
output_tokens=outcome.output_tokens,
60+
steps=getattr(outcome, "steps", 0),
61+
total_reward=getattr(outcome, "total_reward", 0.0),
62+
)
63+
64+
@property
65+
def episodes(self) -> int:
66+
return self._episodes
67+
68+
@property
69+
def wins(self) -> int:
70+
return self._wins
71+
72+
def _stats(self, values: list) -> dict:
73+
if not values:
74+
return {"total": 0, "mean": 0.0, "min": 0, "max": 0}
75+
return {
76+
"total": sum(values),
77+
"mean": sum(values) / len(values),
78+
"min": min(values),
79+
"max": max(values),
80+
}
81+
82+
def summary(self) -> str:
83+
n = self._episodes
84+
if n == 0:
85+
return "RunStats: no episodes recorded."
86+
win_rate = self._wins / n
87+
total_tok = [i + o for i, o in zip(self._input_tokens, self._output_tokens)]
88+
in_s = self._stats(self._input_tokens)
89+
out_s = self._stats(self._output_tokens)
90+
tot_s = self._stats(total_tok)
91+
steps_s = self._stats(self._steps)
92+
rew_s = self._stats(self._rewards)
93+
lines = [
94+
f"=== Run Summary ({n} episodes) ===",
95+
f" win rate : {self._wins}/{n} ({win_rate:.1%})",
96+
f" steps — mean: {steps_s['mean']:>8,.1f} min: {steps_s['min']:>8} max: {steps_s['max']:>8} total: {steps_s['total']:>9,}",
97+
f" reward — mean: {rew_s['mean']:>8,.2f} min: {rew_s['min']:>8.2f} max: {rew_s['max']:>8.2f} total: {rew_s['total']:>9,.2f}",
98+
f" tokens (input) — total: {in_s['total']:>9,} mean: {in_s['mean']:>8,.0f} min: {in_s['min']:>8,} max: {in_s['max']:>8,}",
99+
f" tokens (output) — total: {out_s['total']:>9,} mean: {out_s['mean']:>8,.0f} min: {out_s['min']:>8,} max: {out_s['max']:>8,}",
100+
f" tokens (total) — total: {tot_s['total']:>9,} mean: {tot_s['mean']:>8,.0f} min: {tot_s['min']:>8,} max: {tot_s['max']:>8,}",
101+
]
102+
return "\n".join(lines)
103+
104+
def print_summary(self) -> None:
105+
print(self.summary())

0 commit comments

Comments
 (0)