-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdisplay.py
More file actions
277 lines (222 loc) · 9.39 KB
/
Copy pathdisplay.py
File metadata and controls
277 lines (222 loc) · 9.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"""
Display Module — Clean Rich-based terminal output for agent results.
Each agent gets one panel per cycle. The output is designed to be:
- Scannable: A campaign manager can glance at it and know what happened.
- Non-cluttered: No raw JSON dumps, no debug logs, no wall-of-text.
- Color-coded: Red = critical/escalation, Yellow = warning/diagnosis,
Green = actions taken, Cyan = healthy/info.
The display module is completely decoupled from the agents — it only reads
the final pipeline state and formats it for human consumption. This means
you could swap Rich for Slack messages, email, or a web dashboard without
touching any agent code.
"""
from datetime import datetime
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
# Single console instance — all output goes through this.
console = Console()
def timestamp() -> str:
"""Current time formatted for display headers."""
return datetime.now().strftime("%H:%M:%S")
def print_cycle_header(cycle: int):
"""
Print the cycle separator — a horizontal rule with cycle number and time.
This visually delineates each 30-second monitoring cycle in the terminal.
"""
console.print()
console.rule(f"[bold cyan]Cycle {cycle} · {timestamp()}[/bold cyan]", style="cyan")
console.print()
def print_monitor_summary(alerts: list[dict]):
"""
Print the Monitor Agent's alert table.
Shows one row per alert with campaign name, issue description, severity
(color-coded), and the key metric value that triggered the alert.
If there are no alerts, shows a green "all healthy" panel.
"""
if not alerts:
console.print(
Panel("[green]All campaigns pacing normally[/green]",
title="Monitor Agent", border_style="green")
)
return
# Build a compact table — no borders, just aligned columns.
table = Table(show_header=True, header_style="bold", pad_edge=False, box=None)
table.add_column("Campaign", style="white", min_width=20)
table.add_column("Issue", style="yellow")
table.add_column("Severity", justify="center", min_width=8)
table.add_column("Key Metric", style="dim")
severity_colors = {"critical": "red", "warning": "yellow", "info": "blue"}
for alert in alerts:
sev = alert.get("severity", "info")
color = severity_colors.get(sev, "white")
table.add_row(
alert.get("campaign_name", "?"),
alert.get("issue", "?"),
f"[{color}]{sev.upper()}[/{color}]",
alert.get("metric", ""),
)
console.print(Panel(table, title="[bold yellow]Monitor Agent[/bold yellow]", border_style="yellow"))
def print_diagnosis_summary(diagnoses: list[dict]):
"""
Print the Diagnosis Agent's LLM-generated root cause analysis.
For each diagnosed campaign, shows the root cause and up to 3
contributing factors. The LLM reasoning adds the "why" that rules can't.
"""
if not diagnoses:
return
lines = []
for d in diagnoses:
cname = d.get("campaign_name", "?")
root = d.get("root_cause", "Unknown")
factors = d.get("contributing_factors", [])
lines.append(f"[bold]{cname}[/bold]")
lines.append(f" Root cause: [yellow]{root}[/yellow]")
if factors:
# Show up to 3 contributing factors to keep output concise.
for f in factors[:3]:
lines.append(f" · {f}")
lines.append("") # Blank line between campaigns.
console.print(Panel(
"\n".join(lines).rstrip(),
title="[bold magenta]Diagnosis Agent (LLM)[/bold magenta]",
border_style="magenta",
))
def print_action_summary(actions: list[dict], reasoning: str = ""):
"""
Print the Action Agent's executed actions and LLM reasoning.
Autonomous actions are shown as green checkmarks with descriptions.
The LLM's reasoning trace is shown below if present — this is what
makes the agent's decision-making transparent and auditable.
"""
if not actions and not reasoning:
return
lines = []
# Show autonomous actions that were executed.
if actions:
lines.append("[bold green]Autonomous Actions Taken:[/bold green]")
for a in actions:
cid = a.get("campaign_id", "?")
action_type = a.get("action", "?")
args = a.get("args", {})
# Format action description based on type.
if action_type == "expand_audience":
desc = f"Expanded audience: '{args.get('segment_description', '?')}'"
elif action_type == "rotate_creative":
desc = f"Rotated creative: '{args.get('creative_description', '?')}'"
elif action_type == "adjust_frequency_cap":
desc = f"Frequency cap → {args.get('new_cap', '?')}"
elif action_type == "adjust_daily_budget":
budget = args.get('new_daily_budget', 0)
desc = f"Daily budget → ${budget:,.2f}"
else:
desc = f"{action_type}: {args}"
lines.append(f" [green]✓[/green] [{cid}] {desc}")
# Show truncated LLM reasoning (first 500 chars) for transparency.
if reasoning:
lines.append("")
lines.append("[bold dim]Agent Reasoning:[/bold dim]")
# Truncate long reasoning to keep output scannable.
truncated = reasoning[:500]
if len(reasoning) > 500:
truncated += "..."
lines.append(f" [dim]{truncated}[/dim]")
console.print(Panel(
"\n".join(lines),
title="[bold green]Action Agent (LLM + Tools)[/bold green]",
border_style="green",
))
def print_escalation_summary(notifications: list[dict]):
"""
Print the Escalation Agent's LLM-written campaign manager notifications.
Each notification is formatted as a red-bordered panel with the
LLM-generated narrative text. This is what would be sent via Slack
or email in a production system.
"""
if not notifications:
return
lines = []
for n in notifications:
cname = n.get("campaign_name", "?")
priority = n.get("priority", "normal")
p_color = "red" if priority == "high" else "yellow"
lines.append(
f"[bold]Campaign:[/bold] {cname} "
f"[bold]Priority:[/bold] [{p_color}]{priority.upper()}[/{p_color}]"
)
# The LLM-written notification text — the key output of this agent.
notification_text = n.get("notification_text", "")
if notification_text:
lines.append(f" {notification_text}")
else:
# Fallback: show structured fields if narrative parsing failed.
lines.append(f" Suggested bid: [cyan]${n.get('suggested_bid', '?')}[/cyan]")
lines.append(f" Current bid: ${n.get('current_bid', '?')}")
action = n.get("action_required", "")
if action:
lines.append(f" [bold]Action required:[/bold] {action}")
lines.append(f" [dim]→ Notification sent to campaign manager[/dim]")
lines.append("")
console.print(Panel(
"\n".join(lines).rstrip(),
title="[bold red]Escalation Agent (LLM) — Campaign Manager Notification[/bold red]",
border_style="red",
))
def print_cooldown_status(cooldown_skipped: list[dict]):
"""
Print campaigns that were skipped due to cooldown.
Shows which campaigns have active cooldown and how long until
they become eligible for action again.
"""
if not cooldown_skipped:
return
# Deduplicate by campaign_id (multiple alerts for same campaign).
seen = set()
unique = []
for alert in cooldown_skipped:
cid = alert.get("campaign_id", "?")
if cid not in seen:
seen.add(cid)
unique.append(alert)
lines = []
for alert in unique:
cname = alert.get("campaign_name", "?")
remaining = alert.get("cooldown_remaining", 0)
lines.append(
f" [dim]⏸[/dim] {cname} — "
f"[dim]cooldown {remaining}s remaining (previous fix still propagating)[/dim]"
)
console.print(Panel(
"\n".join(lines),
title="[bold dim]Cooldown — Skipped Campaigns[/bold dim]",
border_style="dim",
))
def print_action_notifications(notifications: list[dict]):
"""
Print FYI notifications about autonomous actions sent to campaign manager.
These are informational — the actions were already executed, but the
campaign manager is notified so they stay in the loop.
"""
if not notifications:
return
lines = []
for n in notifications:
cname = n.get("campaign_name", "?")
actions = n.get("actions_taken", [])
lines.append(f"[bold]Campaign:[/bold] {cname}")
for action_desc in actions:
lines.append(f" [blue]→[/blue] {action_desc}")
lines.append(f" [dim]✉ Notification sent to campaign manager (FYI — no approval needed)[/dim]")
lines.append("")
console.print(Panel(
"\n".join(lines).rstrip(),
title="[bold blue]Campaign Manager Notifications (Autonomous Actions)[/bold blue]",
border_style="blue",
))
def print_no_issues():
"""Print a clean "all healthy" status when no alerts are detected."""
console.print(Panel(
"[green]No issues detected this cycle. All campaigns healthy.[/green]",
title="Status",
border_style="green",
))