agent-scout-cli 0.1.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.
- agent_scout_cli-0.1.0/.gitignore +81 -0
- agent_scout_cli-0.1.0/PKG-INFO +29 -0
- agent_scout_cli-0.1.0/README.md +14 -0
- agent_scout_cli-0.1.0/pyproject.toml +43 -0
- agent_scout_cli-0.1.0/src/agent_scout_cli/__init__.py +3 -0
- agent_scout_cli-0.1.0/src/agent_scout_cli/commands/__init__.py +0 -0
- agent_scout_cli-0.1.0/src/agent_scout_cli/commands/runner.py +256 -0
- agent_scout_cli-0.1.0/src/agent_scout_cli/display/__init__.py +0 -0
- agent_scout_cli-0.1.0/src/agent_scout_cli/display/components.py +181 -0
- agent_scout_cli-0.1.0/src/agent_scout_cli/main.py +445 -0
- agent_scout_cli-0.1.0/tests/__init__.py +0 -0
- agent_scout_cli-0.1.0/tests/test_cli.py +55 -0
- agent_scout_cli-0.1.0/tests/test_display.py +108 -0
- agent_scout_cli-0.1.0/uv.lock +2168 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# ===========================
|
|
2
|
+
# Python
|
|
3
|
+
# ===========================
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
*.so
|
|
8
|
+
*.egg-info/
|
|
9
|
+
*.egg
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
.eggs/
|
|
13
|
+
*.whl
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
.uv/
|
|
17
|
+
.python-version
|
|
18
|
+
|
|
19
|
+
# ===========================
|
|
20
|
+
# Node.js / Next.js
|
|
21
|
+
# ===========================
|
|
22
|
+
node_modules/
|
|
23
|
+
.next/
|
|
24
|
+
out/
|
|
25
|
+
.turbo/
|
|
26
|
+
*.tsbuildinfo
|
|
27
|
+
|
|
28
|
+
# ===========================
|
|
29
|
+
# IDE / Editors
|
|
30
|
+
# ===========================
|
|
31
|
+
.vscode/
|
|
32
|
+
.idea/
|
|
33
|
+
*.swp
|
|
34
|
+
*.swo
|
|
35
|
+
*~
|
|
36
|
+
.DS_Store
|
|
37
|
+
Thumbs.db
|
|
38
|
+
|
|
39
|
+
# ===========================
|
|
40
|
+
# Environment / Secrets
|
|
41
|
+
# ===========================
|
|
42
|
+
.env
|
|
43
|
+
.env.local
|
|
44
|
+
.env.*.local
|
|
45
|
+
!.env.example
|
|
46
|
+
|
|
47
|
+
# ===========================
|
|
48
|
+
# Database
|
|
49
|
+
# ===========================
|
|
50
|
+
*.db
|
|
51
|
+
*.sqlite
|
|
52
|
+
*.sqlite3
|
|
53
|
+
|
|
54
|
+
# ===========================
|
|
55
|
+
# Logs
|
|
56
|
+
# ===========================
|
|
57
|
+
*.log
|
|
58
|
+
logs/
|
|
59
|
+
|
|
60
|
+
# ===========================
|
|
61
|
+
# Testing / Coverage
|
|
62
|
+
# ===========================
|
|
63
|
+
.coverage
|
|
64
|
+
htmlcov/
|
|
65
|
+
.pytest_cache/
|
|
66
|
+
coverage/
|
|
67
|
+
.nyc_output/
|
|
68
|
+
|
|
69
|
+
# ===========================
|
|
70
|
+
# OS
|
|
71
|
+
# ===========================
|
|
72
|
+
.DS_Store
|
|
73
|
+
Thumbs.db
|
|
74
|
+
ehthumbs.db
|
|
75
|
+
Desktop.ini
|
|
76
|
+
|
|
77
|
+
# ===========================
|
|
78
|
+
# Build artifacts
|
|
79
|
+
# ===========================
|
|
80
|
+
*.pyc
|
|
81
|
+
*.pyo
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-scout-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI interface for AgentScout — your personal AI assistant agent
|
|
5
|
+
Author: AgentScout Contributors
|
|
6
|
+
License-Expression: AGPL-3.0-or-later
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: agent-scout-core
|
|
9
|
+
Requires-Dist: rich>=13
|
|
10
|
+
Requires-Dist: typer>=0.24
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# agent-scout-cli
|
|
17
|
+
|
|
18
|
+
CLI interface for AgentScout — your personal AI assistant agent.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
agent-scout run "Find me the best flight from NYC to London on Feb 28"
|
|
24
|
+
agent-scout setup
|
|
25
|
+
agent-scout config show
|
|
26
|
+
agent-scout history
|
|
27
|
+
agent-scout schedule list
|
|
28
|
+
agent-scout web
|
|
29
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# agent-scout-cli
|
|
2
|
+
|
|
3
|
+
CLI interface for AgentScout — your personal AI assistant agent.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
agent-scout run "Find me the best flight from NYC to London on Feb 28"
|
|
9
|
+
agent-scout setup
|
|
10
|
+
agent-scout config show
|
|
11
|
+
agent-scout history
|
|
12
|
+
agent-scout schedule list
|
|
13
|
+
agent-scout web
|
|
14
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agent-scout-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "CLI interface for AgentScout — your personal AI assistant agent"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "AGPL-3.0-or-later"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
authors = [{ name = "AgentScout Contributors" }]
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
"agent-scout-core",
|
|
12
|
+
"typer>=0.24",
|
|
13
|
+
"rich>=13",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=8",
|
|
19
|
+
"ruff>=0.8",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
agent-scout = "agent_scout_cli.main:app"
|
|
24
|
+
|
|
25
|
+
[tool.uv.sources]
|
|
26
|
+
agent-scout-core = { path = "../core", editable = true }
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/agent_scout_cli"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
target-version = "py312"
|
|
40
|
+
line-length = 100
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "SIM"]
|
|
File without changes
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Agent runner — wire up and execute tasks from the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from agent_scout.agent import Agent
|
|
8
|
+
from agent_scout.config.settings import Settings
|
|
9
|
+
from agent_scout.db.models import AutonomyLevel, Task
|
|
10
|
+
from agent_scout.events import Event, EventBus, EventType
|
|
11
|
+
from agent_scout.llm.budget import BudgetConfig, BudgetEngine
|
|
12
|
+
from agent_scout.llm.estimator import CostEstimator
|
|
13
|
+
from agent_scout.llm.provider import (
|
|
14
|
+
AgentLLMProvider,
|
|
15
|
+
CustomEndpoint,
|
|
16
|
+
LLMClient,
|
|
17
|
+
ProviderConfig,
|
|
18
|
+
)
|
|
19
|
+
from agent_scout.llm.router import ModelRouter, RouterConfig
|
|
20
|
+
from agent_scout.plugins.registry import PluginRegistry
|
|
21
|
+
from agent_scout.plugins.web_search import WebSearchPlugin
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
|
|
24
|
+
from agent_scout_cli.display.components import (
|
|
25
|
+
action_panel,
|
|
26
|
+
cost_display,
|
|
27
|
+
error_panel,
|
|
28
|
+
finish_panel,
|
|
29
|
+
observation_panel,
|
|
30
|
+
thought_panel,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_provider_config(settings: Settings) -> ProviderConfig:
|
|
35
|
+
"""Build ProviderConfig from Settings."""
|
|
36
|
+
endpoints = [
|
|
37
|
+
CustomEndpoint(
|
|
38
|
+
name=ep.name,
|
|
39
|
+
base_url=ep.base_url,
|
|
40
|
+
api_key=ep.api_key,
|
|
41
|
+
model_name=ep.model_name,
|
|
42
|
+
)
|
|
43
|
+
for ep in settings.custom_endpoints
|
|
44
|
+
]
|
|
45
|
+
return ProviderConfig(
|
|
46
|
+
default_model=settings.llm.default_model,
|
|
47
|
+
temperature=settings.llm.temperature,
|
|
48
|
+
max_tokens=settings.llm.max_tokens,
|
|
49
|
+
custom_endpoints=endpoints,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _build_router(settings: Settings) -> ModelRouter:
|
|
54
|
+
"""Build ModelRouter from Settings."""
|
|
55
|
+
config = RouterConfig(
|
|
56
|
+
fast_model=settings.llm.fast_model,
|
|
57
|
+
balanced_model=settings.llm.default_model,
|
|
58
|
+
best_model=settings.llm.best_model,
|
|
59
|
+
enabled=settings.llm.enable_routing,
|
|
60
|
+
)
|
|
61
|
+
return ModelRouter(config)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _build_budget(settings: Settings, bus: EventBus) -> BudgetEngine:
|
|
65
|
+
"""Build BudgetEngine from Settings."""
|
|
66
|
+
config = BudgetConfig(
|
|
67
|
+
task_limit=settings.budget.task_limit,
|
|
68
|
+
daily_limit=settings.budget.daily_limit,
|
|
69
|
+
monthly_limit=settings.budget.monthly_limit,
|
|
70
|
+
enforce=settings.budget.enforce,
|
|
71
|
+
)
|
|
72
|
+
return BudgetEngine(config=config, event_bus=bus)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _build_registry(settings: Settings) -> PluginRegistry:
|
|
76
|
+
"""Build and populate the plugin registry."""
|
|
77
|
+
from agent_scout.plugins.base import PluginConfig
|
|
78
|
+
|
|
79
|
+
registry = PluginRegistry()
|
|
80
|
+
|
|
81
|
+
if "web_search" in settings.plugins:
|
|
82
|
+
ws_config = PluginConfig(
|
|
83
|
+
settings={
|
|
84
|
+
"tavily_api_key": settings.search.tavily_api_key,
|
|
85
|
+
"serpapi_api_key": settings.search.serpapi_api_key,
|
|
86
|
+
"brave_api_key": settings.search.brave_api_key,
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
registry.register(WebSearchPlugin(config=ws_config))
|
|
90
|
+
|
|
91
|
+
return registry
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CLIEventHandler:
|
|
95
|
+
"""Display agent events in the terminal."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, console: Console, verbose: bool = False) -> None:
|
|
98
|
+
self._console = console
|
|
99
|
+
self._verbose = verbose
|
|
100
|
+
self._step_count = 0
|
|
101
|
+
self._total_input = 0
|
|
102
|
+
self._total_output = 0
|
|
103
|
+
self._total_cost = 0.0
|
|
104
|
+
|
|
105
|
+
async def handle(self, event: Event) -> None:
|
|
106
|
+
"""Route events to display methods."""
|
|
107
|
+
handlers = {
|
|
108
|
+
EventType.THOUGHT: self._on_thought,
|
|
109
|
+
EventType.ACTION: self._on_action,
|
|
110
|
+
EventType.OBSERVATION: self._on_observation,
|
|
111
|
+
EventType.TOOL_CALL_START: self._on_tool_start,
|
|
112
|
+
EventType.TOOL_CALL_END: self._on_tool_end,
|
|
113
|
+
EventType.TOOL_ERROR: self._on_tool_error,
|
|
114
|
+
EventType.COST_UPDATE: self._on_cost,
|
|
115
|
+
EventType.BUDGET_WARNING: self._on_budget_warning,
|
|
116
|
+
EventType.BUDGET_EXCEEDED: self._on_budget_exceeded,
|
|
117
|
+
EventType.TASK_COMPLETED: self._on_completed,
|
|
118
|
+
EventType.TASK_FAILED: self._on_failed,
|
|
119
|
+
EventType.ERROR: self._on_error,
|
|
120
|
+
}
|
|
121
|
+
handler = handlers.get(event.event_type)
|
|
122
|
+
if handler:
|
|
123
|
+
handler(event)
|
|
124
|
+
|
|
125
|
+
def _on_thought(self, event: Event) -> None:
|
|
126
|
+
self._step_count += 1
|
|
127
|
+
self._console.print(thought_panel(event.data.get("thought", ""), self._step_count))
|
|
128
|
+
|
|
129
|
+
def _on_action(self, event: Event) -> None:
|
|
130
|
+
tool = event.data.get("tool_name", "")
|
|
131
|
+
if tool:
|
|
132
|
+
self._console.print(action_panel(tool, event.data.get("tool_input")))
|
|
133
|
+
|
|
134
|
+
def _on_observation(self, event: Event) -> None:
|
|
135
|
+
obs = event.data.get("observation", "")
|
|
136
|
+
if obs and self._verbose:
|
|
137
|
+
self._console.print(observation_panel(obs))
|
|
138
|
+
elif obs:
|
|
139
|
+
short = obs[:200] + "..." if len(obs) > 200 else obs
|
|
140
|
+
self._console.print(f" [dim]{short}[/dim]")
|
|
141
|
+
|
|
142
|
+
def _on_tool_start(self, event: Event) -> None:
|
|
143
|
+
tool = event.data.get("tool_name", "")
|
|
144
|
+
self._console.print(f" [dim]Running {tool}...[/dim]")
|
|
145
|
+
|
|
146
|
+
def _on_tool_end(self, event: Event) -> None:
|
|
147
|
+
pass # observation handles the output
|
|
148
|
+
|
|
149
|
+
def _on_tool_error(self, event: Event) -> None:
|
|
150
|
+
err = event.data.get("error", "")
|
|
151
|
+
self._console.print(f" [red]Tool error: {err}[/red]")
|
|
152
|
+
|
|
153
|
+
def _on_cost(self, event: Event) -> None:
|
|
154
|
+
self._total_input += event.data.get("input_tokens", 0)
|
|
155
|
+
self._total_output += event.data.get("output_tokens", 0)
|
|
156
|
+
self._total_cost += event.data.get("cost_usd", 0.0)
|
|
157
|
+
|
|
158
|
+
def _on_budget_warning(self, event: Event) -> None:
|
|
159
|
+
pct = event.data.get("percentage", 0)
|
|
160
|
+
limit_type = event.data.get("limit_type", "")
|
|
161
|
+
self._console.print(f" [yellow]Budget warning: {limit_type} at {pct}%[/yellow]")
|
|
162
|
+
|
|
163
|
+
def _on_budget_exceeded(self, event: Event) -> None:
|
|
164
|
+
limit_type = event.data.get("limit_type", "")
|
|
165
|
+
self._console.print(f" [bold red]Budget exceeded: {limit_type}[/bold red]")
|
|
166
|
+
|
|
167
|
+
def _on_completed(self, event: Event) -> None:
|
|
168
|
+
result = event.data.get("result", {})
|
|
169
|
+
message = result.get("message", "") if isinstance(result, dict) else str(result)
|
|
170
|
+
if message:
|
|
171
|
+
self._console.print(finish_panel(message))
|
|
172
|
+
self._print_summary()
|
|
173
|
+
|
|
174
|
+
def _on_failed(self, event: Event) -> None:
|
|
175
|
+
err = event.data.get("error", "Task failed")
|
|
176
|
+
self._console.print(error_panel(err))
|
|
177
|
+
self._print_summary()
|
|
178
|
+
|
|
179
|
+
def _on_error(self, event: Event) -> None:
|
|
180
|
+
err = event.data.get("error", "")
|
|
181
|
+
self._console.print(f" [red]{err}[/red]")
|
|
182
|
+
|
|
183
|
+
def _print_summary(self) -> None:
|
|
184
|
+
if self._total_input > 0 or self._total_output > 0:
|
|
185
|
+
self._console.print(
|
|
186
|
+
cost_display(
|
|
187
|
+
self._total_input,
|
|
188
|
+
self._total_output,
|
|
189
|
+
self._total_cost,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
self._console.print(f"[dim]Completed in {self._step_count} steps[/dim]")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def run_task(
|
|
196
|
+
task_description: str,
|
|
197
|
+
settings: Settings,
|
|
198
|
+
console: Console,
|
|
199
|
+
model_override: str | None = None,
|
|
200
|
+
budget_override: float | None = None,
|
|
201
|
+
verbose: bool = False,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Build the full agent stack and execute a task."""
|
|
204
|
+
bus = EventBus()
|
|
205
|
+
|
|
206
|
+
# Event display
|
|
207
|
+
handler = CLIEventHandler(console, verbose=verbose)
|
|
208
|
+
bus.subscribe(handler.handle)
|
|
209
|
+
|
|
210
|
+
# LLM stack
|
|
211
|
+
provider_config = _build_provider_config(settings)
|
|
212
|
+
client = LLMClient(config=provider_config, event_bus=bus)
|
|
213
|
+
router = _build_router(settings)
|
|
214
|
+
estimator = CostEstimator()
|
|
215
|
+
budget = _build_budget(settings, bus)
|
|
216
|
+
|
|
217
|
+
if budget_override is not None:
|
|
218
|
+
budget.config.task_limit = budget_override
|
|
219
|
+
|
|
220
|
+
# Route to best model for this task
|
|
221
|
+
model = router.route(task_description, override_model=model_override)
|
|
222
|
+
llm_provider = AgentLLMProvider(client, model=model)
|
|
223
|
+
|
|
224
|
+
# Plugin registry
|
|
225
|
+
registry = _build_registry(settings)
|
|
226
|
+
await registry.load_all()
|
|
227
|
+
|
|
228
|
+
# Build autonomy level
|
|
229
|
+
autonomy_map = {
|
|
230
|
+
"full": AutonomyLevel.FULL,
|
|
231
|
+
"semi": AutonomyLevel.SEMI,
|
|
232
|
+
"step": AutonomyLevel.STEP_BY_STEP,
|
|
233
|
+
}
|
|
234
|
+
autonomy = autonomy_map.get(settings.autonomy, AutonomyLevel.FULL)
|
|
235
|
+
|
|
236
|
+
# Pre-execution cost estimate
|
|
237
|
+
estimate = estimator.estimate(model=model, input_tokens=500, expected_output_tokens=1000)
|
|
238
|
+
console.print(f"[dim]Model: {model} | Estimated cost: {estimate.formatted}[/dim]")
|
|
239
|
+
|
|
240
|
+
# Build and run agent
|
|
241
|
+
agent = Agent(
|
|
242
|
+
llm=llm_provider,
|
|
243
|
+
tools=registry,
|
|
244
|
+
event_bus=bus,
|
|
245
|
+
autonomy=autonomy,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
task = Task(
|
|
249
|
+
id=uuid.uuid4().hex[:12],
|
|
250
|
+
description=task_description,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
await agent.run_task(task)
|
|
254
|
+
|
|
255
|
+
# Cleanup
|
|
256
|
+
await registry.unload_all()
|
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Rich display components for the AgentScout CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def thought_panel(thought: str, step_number: int) -> Panel:
|
|
10
|
+
"""Display an agent thought."""
|
|
11
|
+
return Panel(
|
|
12
|
+
thought,
|
|
13
|
+
title=f"[bold cyan]Thought #{step_number}[/bold cyan]",
|
|
14
|
+
border_style="cyan",
|
|
15
|
+
padding=(0, 1),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def action_panel(tool_name: str, tool_input: dict | None = None) -> Panel:
|
|
20
|
+
"""Display an agent action (tool call)."""
|
|
21
|
+
content = f"[bold]{tool_name}[/bold]"
|
|
22
|
+
if tool_input:
|
|
23
|
+
args = ", ".join(f"{k}={v!r}" for k, v in tool_input.items())
|
|
24
|
+
content += f"\n[dim]{args}[/dim]"
|
|
25
|
+
return Panel(
|
|
26
|
+
content,
|
|
27
|
+
title="[bold yellow]Action[/bold yellow]",
|
|
28
|
+
border_style="yellow",
|
|
29
|
+
padding=(0, 1),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def observation_panel(observation: str, max_length: int = 500) -> Panel:
|
|
34
|
+
"""Display an observation (tool result)."""
|
|
35
|
+
display = observation[:max_length]
|
|
36
|
+
if len(observation) > max_length:
|
|
37
|
+
display += f"\n[dim]... ({len(observation) - max_length} chars truncated)[/dim]"
|
|
38
|
+
return Panel(
|
|
39
|
+
display,
|
|
40
|
+
title="[bold green]Observation[/bold green]",
|
|
41
|
+
border_style="green",
|
|
42
|
+
padding=(0, 1),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def finish_panel(message: str) -> Panel:
|
|
47
|
+
"""Display the final result."""
|
|
48
|
+
return Panel(
|
|
49
|
+
message,
|
|
50
|
+
title="[bold green]Result[/bold green]",
|
|
51
|
+
border_style="bold green",
|
|
52
|
+
padding=(1, 2),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def error_panel(error: str) -> Panel:
|
|
57
|
+
"""Display an error."""
|
|
58
|
+
return Panel(
|
|
59
|
+
f"[red]{error}[/red]",
|
|
60
|
+
title="[bold red]Error[/bold red]",
|
|
61
|
+
border_style="red",
|
|
62
|
+
padding=(0, 1),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cost_display(
|
|
67
|
+
input_tokens: int,
|
|
68
|
+
output_tokens: int,
|
|
69
|
+
cost_usd: float,
|
|
70
|
+
budget_remaining: float | None = None,
|
|
71
|
+
) -> Panel:
|
|
72
|
+
"""Display cost information."""
|
|
73
|
+
lines = [
|
|
74
|
+
f"Tokens: [cyan]{input_tokens:,}[/cyan] in / [cyan]{output_tokens:,}[/cyan] out",
|
|
75
|
+
f"Cost: [bold]${cost_usd:.4f}[/bold]",
|
|
76
|
+
]
|
|
77
|
+
if budget_remaining is not None:
|
|
78
|
+
color = "green" if budget_remaining > 0.5 else "yellow" if budget_remaining > 0.1 else "red"
|
|
79
|
+
lines.append(f"Budget remaining: [{color}]${budget_remaining:.2f}[/{color}]")
|
|
80
|
+
return Panel(
|
|
81
|
+
"\n".join(lines),
|
|
82
|
+
title="[bold]Cost[/bold]",
|
|
83
|
+
border_style="dim",
|
|
84
|
+
padding=(0, 1),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def comparison_table(
|
|
89
|
+
title: str,
|
|
90
|
+
columns: list[str],
|
|
91
|
+
rows: list[list[str]],
|
|
92
|
+
highlight_col: int | None = None,
|
|
93
|
+
highlight_row: int | None = 0,
|
|
94
|
+
) -> Table:
|
|
95
|
+
"""Build a comparison table with optional row highlighting."""
|
|
96
|
+
table = Table(title=title, show_lines=True)
|
|
97
|
+
|
|
98
|
+
for i, col in enumerate(columns):
|
|
99
|
+
style = "bold cyan" if i == highlight_col else ""
|
|
100
|
+
table.add_column(col, style=style)
|
|
101
|
+
|
|
102
|
+
for i, row in enumerate(rows):
|
|
103
|
+
style = "bold green" if i == highlight_row else ""
|
|
104
|
+
table.add_row(*row, style=style)
|
|
105
|
+
|
|
106
|
+
return table
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def task_progress(
|
|
110
|
+
step_count: int,
|
|
111
|
+
max_steps: int,
|
|
112
|
+
current_action: str = "",
|
|
113
|
+
) -> Panel:
|
|
114
|
+
"""Display task progress."""
|
|
115
|
+
pct = min(100, int((step_count / max_steps) * 100)) if max_steps > 0 else 0
|
|
116
|
+
bar_filled = pct // 5
|
|
117
|
+
bar_empty = 20 - bar_filled
|
|
118
|
+
bar = f"[green]{'=' * bar_filled}[/green][dim]{'.' * bar_empty}[/dim]"
|
|
119
|
+
|
|
120
|
+
lines = [
|
|
121
|
+
f"Step {step_count}/{max_steps} [{bar}] {pct}%",
|
|
122
|
+
]
|
|
123
|
+
if current_action:
|
|
124
|
+
lines.append(f"[dim]{current_action}[/dim]")
|
|
125
|
+
|
|
126
|
+
return Panel(
|
|
127
|
+
"\n".join(lines),
|
|
128
|
+
title="[bold]Progress[/bold]",
|
|
129
|
+
border_style="blue",
|
|
130
|
+
padding=(0, 1),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def sparkline(values: list[float], width: int = 20) -> str:
|
|
135
|
+
"""Generate a simple text-based sparkline from values."""
|
|
136
|
+
if not values:
|
|
137
|
+
return ""
|
|
138
|
+
|
|
139
|
+
blocks = " ▁▂▃▄▅▆▇█"
|
|
140
|
+
mn, mx = min(values), max(values)
|
|
141
|
+
rng = mx - mn if mx != mn else 1
|
|
142
|
+
|
|
143
|
+
# Sample values to fit width
|
|
144
|
+
if len(values) > width:
|
|
145
|
+
step = len(values) / width
|
|
146
|
+
sampled = [values[int(i * step)] for i in range(width)]
|
|
147
|
+
else:
|
|
148
|
+
sampled = values
|
|
149
|
+
|
|
150
|
+
chars = []
|
|
151
|
+
for v in sampled:
|
|
152
|
+
idx = int(((v - mn) / rng) * (len(blocks) - 1))
|
|
153
|
+
chars.append(blocks[idx])
|
|
154
|
+
return "".join(chars)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def price_trend_display(
|
|
158
|
+
item_name: str,
|
|
159
|
+
prices: list[float],
|
|
160
|
+
currency: str = "USD",
|
|
161
|
+
) -> Panel:
|
|
162
|
+
"""Display a price trend with sparkline."""
|
|
163
|
+
if not prices:
|
|
164
|
+
return Panel(f"No price data for '{item_name}'", border_style="dim")
|
|
165
|
+
|
|
166
|
+
chart = sparkline(prices)
|
|
167
|
+
current = prices[-1]
|
|
168
|
+
mn = min(prices)
|
|
169
|
+
mx = max(prices)
|
|
170
|
+
|
|
171
|
+
lines = [
|
|
172
|
+
f"[bold]{item_name}[/bold]",
|
|
173
|
+
f"Current: {currency} {current:.2f}",
|
|
174
|
+
f"Range: {currency} {mn:.2f} - {currency} {mx:.2f}",
|
|
175
|
+
f"Trend: {chart}",
|
|
176
|
+
]
|
|
177
|
+
return Panel(
|
|
178
|
+
"\n".join(lines),
|
|
179
|
+
border_style="cyan",
|
|
180
|
+
padding=(0, 1),
|
|
181
|
+
)
|