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.
@@ -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"]
@@ -0,0 +1,3 @@
1
+ """AgentScout CLI — Command-line interface for AgentScout."""
2
+
3
+ __version__ = "0.1.0"
@@ -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()
@@ -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
+ )