lambda-agent 0.1.0__py3-none-any.whl
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.
- lambda_agent/__init__.py +0 -0
- lambda_agent/agent.py +279 -0
- lambda_agent/cli_setup.py +43 -0
- lambda_agent/config.py +30 -0
- lambda_agent/context.py +140 -0
- lambda_agent/main.py +318 -0
- lambda_agent/scratchpad.py +139 -0
- lambda_agent/spinner.py +50 -0
- lambda_agent/subagent.py +276 -0
- lambda_agent/tools.py +217 -0
- lambda_agent-0.1.0.dist-info/METADATA +118 -0
- lambda_agent-0.1.0.dist-info/RECORD +16 -0
- lambda_agent-0.1.0.dist-info/WHEEL +5 -0
- lambda_agent-0.1.0.dist-info/entry_points.txt +2 -0
- lambda_agent-0.1.0.dist-info/licenses/LICENSE +201 -0
- lambda_agent-0.1.0.dist-info/top_level.txt +1 -0
lambda_agent/main.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from .agent import Agent, TokenUsage
|
|
2
|
+
from . import config
|
|
3
|
+
from .spinner import console
|
|
4
|
+
import os
|
|
5
|
+
import getpass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from rich.rule import Rule
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from rich.prompt import Prompt
|
|
13
|
+
from rich import box
|
|
14
|
+
from rich.align import Align
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
BANNER = r"""
|
|
19
|
+
██╗ █████╗ ███╗ ███╗██████╗ ██████╗ █████╗
|
|
20
|
+
██║ ██╔══██╗████╗ ████║██╔══██╗██╔══██╗██╔══██╗
|
|
21
|
+
██║ ███████║██╔████╔██║██████╔╝██║ ██║███████║
|
|
22
|
+
██║ ██╔══██║██║╚██╔╝██║██╔══██╗██║ ██║██╔══██║
|
|
23
|
+
███████╗██║ ██║██║ ╚═╝ ██║██████╔╝██████╔╝██║ ██║
|
|
24
|
+
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
SLASH_COMMANDS = {
|
|
29
|
+
"/models": "List available models and switch between them",
|
|
30
|
+
"/config": "Update API key and save to config",
|
|
31
|
+
"/help": "Show available slash commands",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def print_banner():
|
|
36
|
+
banner_text = Text(BANNER, style="bold cyan", justify="center")
|
|
37
|
+
subtitle = Text(
|
|
38
|
+
" Minimal AI Coding Agent · Type '/help' for commands ",
|
|
39
|
+
style="dim white",
|
|
40
|
+
justify="center",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
panel = Panel(
|
|
44
|
+
Align.center(Text.assemble(banner_text, "\n", subtitle)),
|
|
45
|
+
border_style="cyan",
|
|
46
|
+
box=box.DOUBLE_EDGE,
|
|
47
|
+
padding=(0, 2),
|
|
48
|
+
)
|
|
49
|
+
console.print(panel)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def print_user_message(text: str):
|
|
53
|
+
label = Text(" YOU ", style="bold black on bright_yellow")
|
|
54
|
+
content = Text(f" {text}", style="bright_white")
|
|
55
|
+
console.print()
|
|
56
|
+
console.print(Text.assemble(label, content))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def print_lambda_message(text: str):
|
|
60
|
+
console.print()
|
|
61
|
+
label = Text(" LAMBDA ", style="bold black on cyan")
|
|
62
|
+
console.print(label)
|
|
63
|
+
console.print(
|
|
64
|
+
Panel(
|
|
65
|
+
Markdown(text),
|
|
66
|
+
border_style="cyan",
|
|
67
|
+
box=box.ROUNDED,
|
|
68
|
+
padding=(0, 2),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def print_token_stats(turn: TokenUsage, session: TokenUsage):
|
|
74
|
+
"""Render a compact token usage line under the Lambda response."""
|
|
75
|
+
console.print(
|
|
76
|
+
Text.assemble(
|
|
77
|
+
(" ▶ tokens ", "dim"),
|
|
78
|
+
("this turn: ", "dim"),
|
|
79
|
+
(f"↑{turn.prompt:,}", "dim cyan"),
|
|
80
|
+
(" in ", "dim"),
|
|
81
|
+
(f"↓{turn.completion:,}", "dim cyan"),
|
|
82
|
+
(" out ", "dim"),
|
|
83
|
+
("session total: ", "dim"),
|
|
84
|
+
(f"{session.total:,}", "bold cyan"),
|
|
85
|
+
(" tokens", "dim"),
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def handle_models_command(agent: Agent):
|
|
91
|
+
"""Display available models and let the user pick one."""
|
|
92
|
+
table = Table(
|
|
93
|
+
title="Available Models",
|
|
94
|
+
title_style="bold cyan",
|
|
95
|
+
border_style="cyan",
|
|
96
|
+
box=box.ROUNDED,
|
|
97
|
+
padding=(0, 2),
|
|
98
|
+
)
|
|
99
|
+
table.add_column("#", style="dim", width=4)
|
|
100
|
+
table.add_column("Model", style="white")
|
|
101
|
+
table.add_column("Status", justify="center")
|
|
102
|
+
|
|
103
|
+
models = config.AVAILABLE_MODELS
|
|
104
|
+
for i, model in enumerate(models, 1):
|
|
105
|
+
is_active = model == agent.model_name
|
|
106
|
+
status = "[bold green]● active[/bold green]" if is_active else "[dim]—[/dim]"
|
|
107
|
+
name_style = "bold cyan" if is_active else "white"
|
|
108
|
+
table.add_row(str(i), f"[{name_style}]{model}[/{name_style}]", status)
|
|
109
|
+
|
|
110
|
+
console.print()
|
|
111
|
+
console.print(table)
|
|
112
|
+
console.print()
|
|
113
|
+
|
|
114
|
+
choice = Prompt.ask(
|
|
115
|
+
"[bold bright_yellow] Select model #[/bold bright_yellow] (or Enter to cancel)",
|
|
116
|
+
default="",
|
|
117
|
+
console=console,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if not choice.strip():
|
|
121
|
+
console.print(" [dim]No change.[/dim]")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
idx = int(choice) - 1
|
|
126
|
+
if 0 <= idx < len(models):
|
|
127
|
+
selected = models[idx]
|
|
128
|
+
if selected == agent.model_name:
|
|
129
|
+
console.print(f" [dim]Already using[/dim] [cyan]{selected}[/cyan]")
|
|
130
|
+
else:
|
|
131
|
+
msg = agent.switch_model(selected)
|
|
132
|
+
console.print(f" {msg}")
|
|
133
|
+
else:
|
|
134
|
+
console.print(" [red]Invalid selection.[/red]")
|
|
135
|
+
except ValueError:
|
|
136
|
+
console.print(" [red]Please enter a number.[/red]")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def handle_help_command():
|
|
140
|
+
"""Show available slash commands."""
|
|
141
|
+
table = Table(
|
|
142
|
+
title="Slash Commands",
|
|
143
|
+
title_style="bold cyan",
|
|
144
|
+
border_style="cyan",
|
|
145
|
+
box=box.ROUNDED,
|
|
146
|
+
padding=(0, 1),
|
|
147
|
+
)
|
|
148
|
+
table.add_column("Command", style="bold bright_yellow", min_width=12)
|
|
149
|
+
table.add_column("Description", style="white")
|
|
150
|
+
|
|
151
|
+
for cmd, desc in SLASH_COMMANDS.items():
|
|
152
|
+
table.add_row(cmd, desc)
|
|
153
|
+
|
|
154
|
+
# Also list the built-in exit commands
|
|
155
|
+
table.add_row("exit / quit", "End the session")
|
|
156
|
+
|
|
157
|
+
console.print()
|
|
158
|
+
console.print(table)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def handle_config_command(agent: Agent):
|
|
162
|
+
"""Let the user update their API key mid-session."""
|
|
163
|
+
console.print()
|
|
164
|
+
console.print(
|
|
165
|
+
Panel(
|
|
166
|
+
Text.assemble(
|
|
167
|
+
("Current API key: ", "dim"),
|
|
168
|
+
(f"{config.API_KEY[:8]}...{config.API_KEY[-4:]}", "cyan"),
|
|
169
|
+
),
|
|
170
|
+
border_style="cyan",
|
|
171
|
+
box=box.ROUNDED,
|
|
172
|
+
title="[bold cyan]⚙ Configuration[/bold cyan]",
|
|
173
|
+
title_align="left",
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
console.print()
|
|
177
|
+
|
|
178
|
+
new_key = getpass.getpass(" Enter new API key (or press Enter to keep current): ")
|
|
179
|
+
|
|
180
|
+
if not new_key.strip():
|
|
181
|
+
console.print(" [dim]No change.[/dim]")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Update in-memory config
|
|
185
|
+
config.API_KEY = new_key.strip()
|
|
186
|
+
os.environ["API_KEY"] = config.API_KEY
|
|
187
|
+
|
|
188
|
+
# Re-create the API client with the new key
|
|
189
|
+
from google import genai
|
|
190
|
+
|
|
191
|
+
agent.client = genai.Client(api_key=config.API_KEY)
|
|
192
|
+
|
|
193
|
+
# Re-create the chat session so the new client is used
|
|
194
|
+
from google.genai import types
|
|
195
|
+
from .tools import TOOL_FUNCTIONS
|
|
196
|
+
|
|
197
|
+
agent.chat_session = agent.client.chats.create(
|
|
198
|
+
model=agent.model_name,
|
|
199
|
+
config=types.GenerateContentConfig(
|
|
200
|
+
system_instruction=agent.system_instruction,
|
|
201
|
+
tools=TOOL_FUNCTIONS,
|
|
202
|
+
automatic_function_calling=types.AutomaticFunctionCallingConfig(
|
|
203
|
+
disable=True
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
agent.is_first_message = True
|
|
208
|
+
|
|
209
|
+
# Persist to config file
|
|
210
|
+
config_file = Path.home() / ".config" / "lambda-agent" / "config.env"
|
|
211
|
+
try:
|
|
212
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
with open(config_file, "w") as f:
|
|
214
|
+
f.write(f"API_KEY={config.API_KEY}\n")
|
|
215
|
+
f.write(f"MODEL_NAME={config.MODEL_NAME}\n")
|
|
216
|
+
os.chmod(config_file, 0o600)
|
|
217
|
+
console.print(" [green]✓[/green] API key updated and saved to config.")
|
|
218
|
+
except Exception as e:
|
|
219
|
+
console.print(" [green]✓[/green] API key updated in memory.")
|
|
220
|
+
console.print(f" [yellow]⚠[/yellow] Could not save to disk: {e}")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main():
|
|
224
|
+
print_banner()
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
if not config.API_KEY:
|
|
228
|
+
from .cli_setup import run_setup
|
|
229
|
+
|
|
230
|
+
config.API_KEY, config.MODEL_NAME = run_setup()
|
|
231
|
+
os.environ["API_KEY"] = config.API_KEY
|
|
232
|
+
os.environ["MODEL_NAME"] = config.MODEL_NAME
|
|
233
|
+
|
|
234
|
+
agent = Agent()
|
|
235
|
+
|
|
236
|
+
console.print(
|
|
237
|
+
Rule("[bold cyan]Session Started[/bold cyan]", style="cyan"),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
while True:
|
|
241
|
+
try:
|
|
242
|
+
# Styled prompt — uses plain input to keep cursor on same line
|
|
243
|
+
user_input = Prompt.ask(
|
|
244
|
+
"\n[bold bright_yellow] You[/bold bright_yellow]",
|
|
245
|
+
console=console,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if user_input.lower() in ["exit", "quit"]:
|
|
249
|
+
console.print()
|
|
250
|
+
# Show session token summary before quitting
|
|
251
|
+
if agent.token_usage.total > 0:
|
|
252
|
+
console.print(
|
|
253
|
+
Panel(
|
|
254
|
+
Text.assemble(
|
|
255
|
+
("Session token usage\n", "bold white"),
|
|
256
|
+
(" Prompt (in): ", "dim"),
|
|
257
|
+
(f"{agent.token_usage.prompt:>10,}\n", "cyan"),
|
|
258
|
+
(" Completion (out): ", "dim"),
|
|
259
|
+
(f"{agent.token_usage.completion:>10,}\n", "cyan"),
|
|
260
|
+
(" Total: ", "dim"),
|
|
261
|
+
(f"{agent.token_usage.total:>10,}", "bold cyan"),
|
|
262
|
+
),
|
|
263
|
+
border_style="cyan",
|
|
264
|
+
box=box.ROUNDED,
|
|
265
|
+
title="[bold cyan]⚡ Token Summary[/bold cyan]",
|
|
266
|
+
title_align="left",
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
console.print(
|
|
270
|
+
Panel(
|
|
271
|
+
"[bold cyan]Goodbye! Lambda signing off.[/bold cyan]",
|
|
272
|
+
border_style="cyan",
|
|
273
|
+
box=box.ROUNDED,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
break
|
|
277
|
+
|
|
278
|
+
if not user_input.strip():
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# Handle slash commands
|
|
282
|
+
if user_input.strip().lower() == "/models":
|
|
283
|
+
handle_models_command(agent)
|
|
284
|
+
continue
|
|
285
|
+
elif user_input.strip().lower() == "/config":
|
|
286
|
+
handle_config_command(agent)
|
|
287
|
+
continue
|
|
288
|
+
elif user_input.strip().lower() == "/help":
|
|
289
|
+
handle_help_command()
|
|
290
|
+
continue
|
|
291
|
+
elif user_input.strip().startswith("/"):
|
|
292
|
+
console.print(
|
|
293
|
+
f" [red]Unknown command:[/red] {user_input.strip()} "
|
|
294
|
+
"[dim]Type /help for available commands.[/dim]"
|
|
295
|
+
)
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
response, turn_usage = agent.chat(user_input)
|
|
299
|
+
print_lambda_message(response)
|
|
300
|
+
print_token_stats(turn_usage, agent.token_usage)
|
|
301
|
+
|
|
302
|
+
except KeyboardInterrupt:
|
|
303
|
+
console.print()
|
|
304
|
+
console.print("[bold cyan]\nGoodbye![/bold cyan]")
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
except Exception as e:
|
|
308
|
+
console.print(
|
|
309
|
+
Panel(
|
|
310
|
+
f"[bold red]Failed to initialize Lambda:[/bold red]\n{str(e)}",
|
|
311
|
+
border_style="red",
|
|
312
|
+
box=box.ROUNDED,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == "__main__":
|
|
318
|
+
main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scratchpad Module
|
|
3
|
+
=================
|
|
4
|
+
Provides tools for the agent to maintain a persistent, human-readable scratchpad
|
|
5
|
+
file (.lambda_scratchpad.md) in the user's working directory.
|
|
6
|
+
|
|
7
|
+
The scratchpad lets the agent plan complex tasks, track progress, and keep
|
|
8
|
+
implementation notes — all visible to the user as a markdown file in the repo.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
AGENT_DIR = ".agent"
|
|
15
|
+
SCRATCHPAD_FILE = os.path.join(AGENT_DIR, "scratchpad.md")
|
|
16
|
+
|
|
17
|
+
_HEADER_TEMPLATE = """\
|
|
18
|
+
<!-- This file is managed by the Lambda coding agent. -->
|
|
19
|
+
<!-- Feel free to read it, but edits may be overwritten by the agent. -->
|
|
20
|
+
|
|
21
|
+
# Lambda Scratchpad
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ensure_scratchpad() -> str:
|
|
27
|
+
"""Return the absolute path to the scratchpad, creating it if it doesn't exist."""
|
|
28
|
+
agent_dir = os.path.abspath(AGENT_DIR)
|
|
29
|
+
os.makedirs(agent_dir, exist_ok=True)
|
|
30
|
+
path = os.path.abspath(SCRATCHPAD_FILE)
|
|
31
|
+
if not os.path.exists(path):
|
|
32
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
33
|
+
f.write(_HEADER_TEMPLATE)
|
|
34
|
+
return path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_scratchpad() -> str:
|
|
38
|
+
"""Reads the full contents of the Lambda scratchpad file (.lambda_scratchpad.md).
|
|
39
|
+
|
|
40
|
+
Use this to recall your previous plans, task lists, and implementation notes.
|
|
41
|
+
"""
|
|
42
|
+
path = _ensure_scratchpad()
|
|
43
|
+
try:
|
|
44
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
45
|
+
return f.read()
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return f"Error reading scratchpad: {e}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def write_scratchpad(content: str) -> str:
|
|
51
|
+
"""Overwrites the entire Lambda scratchpad file with the provided content.
|
|
52
|
+
|
|
53
|
+
Use this when you need to replace the scratchpad with a fresh plan or when
|
|
54
|
+
starting a new major task. For incremental updates, prefer update_scratchpad.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
content: The full markdown content to write to the scratchpad.
|
|
58
|
+
"""
|
|
59
|
+
path = _ensure_scratchpad()
|
|
60
|
+
try:
|
|
61
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
62
|
+
f.write(_HEADER_TEMPLATE + content)
|
|
63
|
+
return f"Scratchpad written successfully → {path}"
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return f"Error writing scratchpad: {e}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def update_scratchpad(note: str, section: str = "Notes") -> str:
|
|
69
|
+
"""Appends a timestamped note to a specific section in the scratchpad.
|
|
70
|
+
|
|
71
|
+
This is ideal for incrementally logging progress, decisions, and discoveries
|
|
72
|
+
without replacing existing content.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
note: The text to append (supports markdown).
|
|
76
|
+
section: The section heading to append under (e.g. 'Plan', 'Progress', 'Notes').
|
|
77
|
+
"""
|
|
78
|
+
path = _ensure_scratchpad()
|
|
79
|
+
try:
|
|
80
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
81
|
+
existing = f.read()
|
|
82
|
+
|
|
83
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
84
|
+
entry = f"\n- **[{timestamp}]** {note}"
|
|
85
|
+
|
|
86
|
+
section_heading = f"## {section}"
|
|
87
|
+
if section_heading in existing:
|
|
88
|
+
# Append under the existing section
|
|
89
|
+
parts = existing.split(section_heading, 1)
|
|
90
|
+
# Find the next section heading (##) or end of file
|
|
91
|
+
rest = parts[1]
|
|
92
|
+
next_section = rest.find("\n## ")
|
|
93
|
+
if next_section == -1:
|
|
94
|
+
# No next section — just append at the end
|
|
95
|
+
updated = existing + entry
|
|
96
|
+
else:
|
|
97
|
+
# Insert before the next section
|
|
98
|
+
insert_pos = len(parts[0]) + len(section_heading) + next_section
|
|
99
|
+
updated = existing[:insert_pos] + entry + "\n" + existing[insert_pos:]
|
|
100
|
+
else:
|
|
101
|
+
# Create the section at the end
|
|
102
|
+
updated = existing.rstrip() + f"\n\n{section_heading}\n{entry}\n"
|
|
103
|
+
|
|
104
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
105
|
+
f.write(updated)
|
|
106
|
+
|
|
107
|
+
return f"Scratchpad updated (section: {section}) → {path}"
|
|
108
|
+
except Exception as e:
|
|
109
|
+
return f"Error updating scratchpad: {e}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def clear_scratchpad() -> str:
|
|
113
|
+
"""Clears the scratchpad, resetting it to a blank state.
|
|
114
|
+
|
|
115
|
+
Use this when a major task is fully complete and the scratchpad is no longer needed.
|
|
116
|
+
"""
|
|
117
|
+
path = _ensure_scratchpad()
|
|
118
|
+
try:
|
|
119
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
120
|
+
f.write(_HEADER_TEMPLATE)
|
|
121
|
+
return f"Scratchpad cleared → {path}"
|
|
122
|
+
except Exception as e:
|
|
123
|
+
return f"Error clearing scratchpad: {e}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Tool registrations for the agent
|
|
127
|
+
SCRATCHPAD_EXECUTORS = {
|
|
128
|
+
"read_scratchpad": read_scratchpad,
|
|
129
|
+
"write_scratchpad": write_scratchpad,
|
|
130
|
+
"update_scratchpad": update_scratchpad,
|
|
131
|
+
"clear_scratchpad": clear_scratchpad,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
SCRATCHPAD_FUNCTIONS = [
|
|
135
|
+
read_scratchpad,
|
|
136
|
+
write_scratchpad,
|
|
137
|
+
update_scratchpad,
|
|
138
|
+
clear_scratchpad,
|
|
139
|
+
]
|
lambda_agent/spinner.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.spinner import Spinner as RichSpinner
|
|
4
|
+
from rich.live import Live
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
# Shared console instance used across the whole app
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
QUOTES = [
|
|
11
|
+
"Consulting the mainframe…",
|
|
12
|
+
"Synthesizing logic…",
|
|
13
|
+
"Bending the matrix…",
|
|
14
|
+
"Drinking virtual coffee…",
|
|
15
|
+
"Compiling thoughts…",
|
|
16
|
+
"Evaluating your code…",
|
|
17
|
+
"Simulating outcomes…",
|
|
18
|
+
"Reversing the polarity…",
|
|
19
|
+
"Aligning the vectors…",
|
|
20
|
+
"Traversing the graph…",
|
|
21
|
+
"Reading your source code…",
|
|
22
|
+
"Assembling the bytes…",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Spinner:
|
|
27
|
+
"""Context manager that shows a rich animated spinner while Lambda is thinking."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str = "Thinking"):
|
|
30
|
+
self._label = random.choice(QUOTES) if random.random() > 0.3 else message
|
|
31
|
+
self._live = None
|
|
32
|
+
|
|
33
|
+
def __enter__(self):
|
|
34
|
+
renderable = RichSpinner(
|
|
35
|
+
"dots",
|
|
36
|
+
text=Text(f" {self._label}", style="dim cyan italic"),
|
|
37
|
+
style="bold cyan",
|
|
38
|
+
)
|
|
39
|
+
self._live = Live(
|
|
40
|
+
renderable,
|
|
41
|
+
console=console,
|
|
42
|
+
refresh_per_second=15,
|
|
43
|
+
transient=True,
|
|
44
|
+
)
|
|
45
|
+
self._live.__enter__()
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
49
|
+
if self._live:
|
|
50
|
+
self._live.__exit__(exc_type, exc_val, exc_tb)
|