tsugite-cli 0.3.3__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.
- tsugite/__init__.py +6 -0
- tsugite/agent_composition.py +163 -0
- tsugite/agent_inheritance.py +479 -0
- tsugite/agent_preparation.py +236 -0
- tsugite/agent_runner/__init__.py +45 -0
- tsugite/agent_runner/helpers.py +106 -0
- tsugite/agent_runner/history_integration.py +248 -0
- tsugite/agent_runner/metrics.py +100 -0
- tsugite/agent_runner/runner.py +1879 -0
- tsugite/agent_runner/validation.py +70 -0
- tsugite/agent_utils.py +167 -0
- tsugite/attachments/__init__.py +65 -0
- tsugite/attachments/auto_context.py +199 -0
- tsugite/attachments/base.py +34 -0
- tsugite/attachments/file.py +51 -0
- tsugite/attachments/inline.py +31 -0
- tsugite/attachments/storage.py +178 -0
- tsugite/attachments/url.py +59 -0
- tsugite/attachments/youtube.py +101 -0
- tsugite/benchmark/__init__.py +62 -0
- tsugite/benchmark/config.py +183 -0
- tsugite/benchmark/core.py +292 -0
- tsugite/benchmark/discovery.py +377 -0
- tsugite/benchmark/evaluators.py +671 -0
- tsugite/benchmark/execution.py +657 -0
- tsugite/benchmark/metrics.py +204 -0
- tsugite/benchmark/reports.py +420 -0
- tsugite/benchmark/utils.py +288 -0
- tsugite/builtin_agents/chat-assistant.md +53 -0
- tsugite/builtin_agents/default.md +140 -0
- tsugite/builtin_agents.py +5 -0
- tsugite/cache.py +195 -0
- tsugite/cli/__init__.py +1042 -0
- tsugite/cli/agents.py +148 -0
- tsugite/cli/attachments.py +193 -0
- tsugite/cli/benchmark.py +663 -0
- tsugite/cli/cache.py +113 -0
- tsugite/cli/config.py +272 -0
- tsugite/cli/helpers.py +534 -0
- tsugite/cli/history.py +193 -0
- tsugite/cli/init.py +387 -0
- tsugite/cli/mcp.py +193 -0
- tsugite/cli/tools.py +419 -0
- tsugite/config.py +204 -0
- tsugite/console.py +48 -0
- tsugite/constants.py +21 -0
- tsugite/core/__init__.py +19 -0
- tsugite/core/agent.py +774 -0
- tsugite/core/executor.py +300 -0
- tsugite/core/memory.py +67 -0
- tsugite/core/tools.py +271 -0
- tsugite/docker_cli.py +270 -0
- tsugite/events/__init__.py +55 -0
- tsugite/events/base.py +46 -0
- tsugite/events/bus.py +62 -0
- tsugite/events/events.py +224 -0
- tsugite/exceptions.py +40 -0
- tsugite/history/__init__.py +29 -0
- tsugite/history/index.py +210 -0
- tsugite/history/models.py +106 -0
- tsugite/history/storage.py +157 -0
- tsugite/mcp_client.py +219 -0
- tsugite/mcp_config.py +174 -0
- tsugite/md_agents.py +751 -0
- tsugite/models.py +257 -0
- tsugite/renderer.py +151 -0
- tsugite/shell_tool_config.py +265 -0
- tsugite/templates/assistant.md +14 -0
- tsugite/tools/__init__.py +265 -0
- tsugite/tools/agents.py +312 -0
- tsugite/tools/edit_strategies.py +393 -0
- tsugite/tools/fs.py +329 -0
- tsugite/tools/http.py +239 -0
- tsugite/tools/interactive.py +430 -0
- tsugite/tools/shell.py +129 -0
- tsugite/tools/shell_tools.py +214 -0
- tsugite/tools/tasks.py +339 -0
- tsugite/tsugite.py +7 -0
- tsugite/ui/__init__.py +46 -0
- tsugite/ui/base.py +638 -0
- tsugite/ui/chat.py +265 -0
- tsugite/ui/chat.tcss +92 -0
- tsugite/ui/chat_history.py +286 -0
- tsugite/ui/helpers.py +102 -0
- tsugite/ui/jsonl.py +125 -0
- tsugite/ui/live_template.py +529 -0
- tsugite/ui/plain.py +419 -0
- tsugite/ui/textual_chat.py +642 -0
- tsugite/ui/textual_handler.py +225 -0
- tsugite/ui/widgets/__init__.py +6 -0
- tsugite/ui/widgets/base_scroll_log.py +27 -0
- tsugite/ui/widgets/message_list.py +121 -0
- tsugite/ui/widgets/thought_log.py +80 -0
- tsugite/ui_context.py +90 -0
- tsugite/utils.py +367 -0
- tsugite/xdg.py +104 -0
- tsugite_cli-0.3.3.dist-info/METADATA +325 -0
- tsugite_cli-0.3.3.dist-info/RECORD +101 -0
- tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
- tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
- tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
tsugite/cli/history.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""History management CLI commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from tsugite.history import (
|
|
11
|
+
ConversationMetadata,
|
|
12
|
+
Turn,
|
|
13
|
+
get_history_dir,
|
|
14
|
+
load_conversation,
|
|
15
|
+
query_index,
|
|
16
|
+
rebuild_index,
|
|
17
|
+
)
|
|
18
|
+
from tsugite.ui.chat_history import format_conversation_for_display
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
history_app = typer.Typer(help="Manage conversation history")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@history_app.command("list")
|
|
26
|
+
def history_list(
|
|
27
|
+
machine: str = typer.Option(None, "--machine", help="Filter by machine name"),
|
|
28
|
+
agent: str = typer.Option(None, "--agent", help="Filter by agent name"),
|
|
29
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Maximum number of results"),
|
|
30
|
+
):
|
|
31
|
+
"""List conversations with optional filters.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
tsugite history list
|
|
35
|
+
tsugite history list --machine laptop
|
|
36
|
+
tsugite history list --agent chat_assistant --limit 10
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
conversations = query_index(machine=machine, agent=agent, limit=limit)
|
|
40
|
+
|
|
41
|
+
if not conversations:
|
|
42
|
+
console.print("[yellow]No conversations found[/yellow]")
|
|
43
|
+
|
|
44
|
+
history_dir = get_history_dir()
|
|
45
|
+
if not history_dir.exists():
|
|
46
|
+
console.print(f"\nHistory directory doesn't exist yet: {history_dir}")
|
|
47
|
+
console.print("Conversations will be saved automatically when you use chat mode.")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
table = Table(title=f"Conversation History ({len(conversations)} found)")
|
|
51
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
52
|
+
table.add_column("Agent", style="green")
|
|
53
|
+
table.add_column("Machine", style="dim")
|
|
54
|
+
table.add_column("Turns", justify="right")
|
|
55
|
+
table.add_column("Tokens", justify="right", style="dim")
|
|
56
|
+
table.add_column("Updated", style="dim")
|
|
57
|
+
|
|
58
|
+
for conv in conversations:
|
|
59
|
+
conv_id = conv.get("conversation_id", "unknown")
|
|
60
|
+
agent_name = conv.get("agent", "unknown")
|
|
61
|
+
machine_name = conv.get("machine", "unknown")
|
|
62
|
+
turn_count = conv.get("turn_count", 0)
|
|
63
|
+
total_tokens = conv.get("total_tokens", 0)
|
|
64
|
+
updated_at = conv.get("updated_at", "unknown")
|
|
65
|
+
|
|
66
|
+
# Format date
|
|
67
|
+
try:
|
|
68
|
+
dt = datetime.fromisoformat(updated_at)
|
|
69
|
+
updated_str = dt.strftime("%Y-%m-%d %H:%M")
|
|
70
|
+
except (ValueError, TypeError):
|
|
71
|
+
updated_str = updated_at[:16] if len(updated_at) > 16 else updated_at
|
|
72
|
+
|
|
73
|
+
table.add_row(
|
|
74
|
+
conv_id,
|
|
75
|
+
agent_name,
|
|
76
|
+
machine_name,
|
|
77
|
+
str(turn_count),
|
|
78
|
+
f"{total_tokens:,}" if total_tokens > 0 else "-",
|
|
79
|
+
updated_str,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
console.print(table)
|
|
83
|
+
|
|
84
|
+
# Show usage hint
|
|
85
|
+
console.print("\n[dim]Use 'tsugite history show CONVERSATION_ID' to view details[/dim]")
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
console.print(f"[red]Failed to list conversations: {e}[/red]")
|
|
89
|
+
raise typer.Exit(1)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@history_app.command("show")
|
|
93
|
+
def history_show(
|
|
94
|
+
conversation_id: str = typer.Argument(help="Conversation ID to show"),
|
|
95
|
+
format: str = typer.Option("plain", "--format", "-f", help="Output format (plain, json, markdown)"),
|
|
96
|
+
):
|
|
97
|
+
"""Show full conversation details.
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
tsugite history show 20251024_103000_chat_abc123
|
|
101
|
+
tsugite history show 20251024_103000_chat_abc123 --format json
|
|
102
|
+
tsugite history show 20251024_103000_chat_abc123 --format markdown
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
turns = load_conversation(conversation_id)
|
|
106
|
+
|
|
107
|
+
if not turns:
|
|
108
|
+
console.print(f"[yellow]Conversation '{conversation_id}' is empty[/yellow]")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if format == "json":
|
|
112
|
+
# JSON output - convert Pydantic models to dicts
|
|
113
|
+
turns_as_dicts = [t.model_dump(mode="json") for t in turns]
|
|
114
|
+
output = json.dumps(turns_as_dicts, indent=2, ensure_ascii=False)
|
|
115
|
+
# Use plain print to avoid Rich wrapping that breaks JSON
|
|
116
|
+
print(output)
|
|
117
|
+
|
|
118
|
+
elif format == "markdown":
|
|
119
|
+
# Markdown output
|
|
120
|
+
metadata = next((t for t in turns if isinstance(t, ConversationMetadata)), None)
|
|
121
|
+
|
|
122
|
+
console.print(f"# Conversation: {conversation_id}\n")
|
|
123
|
+
if metadata:
|
|
124
|
+
console.print(f"- **Agent**: {metadata.agent}")
|
|
125
|
+
console.print(f"- **Model**: {metadata.model}")
|
|
126
|
+
console.print(f"- **Machine**: {metadata.machine}")
|
|
127
|
+
console.print(f"- **Created**: {metadata.created_at}\n")
|
|
128
|
+
console.print("---\n")
|
|
129
|
+
|
|
130
|
+
for turn in turns:
|
|
131
|
+
if isinstance(turn, Turn):
|
|
132
|
+
console.print(f"## Turn ({turn.timestamp})\n")
|
|
133
|
+
console.print(f"**User**: {turn.user}\n")
|
|
134
|
+
console.print(f"**Assistant**: {turn.assistant}\n")
|
|
135
|
+
|
|
136
|
+
if turn.tools:
|
|
137
|
+
console.print(f"*Tools*: {', '.join(turn.tools)}\n")
|
|
138
|
+
|
|
139
|
+
# Display execution steps if available
|
|
140
|
+
if turn.steps:
|
|
141
|
+
console.print("### Execution Steps\n")
|
|
142
|
+
for step in turn.steps:
|
|
143
|
+
step_num = step.get("step_number", "?")
|
|
144
|
+
thought = step.get("thought", "").strip()
|
|
145
|
+
code = step.get("code", "").strip()
|
|
146
|
+
output = step.get("output", "").strip()
|
|
147
|
+
error = step.get("error")
|
|
148
|
+
tools_called = step.get("tools_called", [])
|
|
149
|
+
|
|
150
|
+
console.print(f"**Step {step_num}**\n")
|
|
151
|
+
if thought:
|
|
152
|
+
console.print(f"*Thought*: {thought}\n")
|
|
153
|
+
if tools_called:
|
|
154
|
+
console.print(f"*Tools used*: {', '.join(tools_called)}\n")
|
|
155
|
+
if code:
|
|
156
|
+
console.print("*Code*:")
|
|
157
|
+
console.print(f"```python\n{code}\n```\n")
|
|
158
|
+
if output:
|
|
159
|
+
console.print(f"*Output*: {output}\n")
|
|
160
|
+
if error:
|
|
161
|
+
console.print(f"*Error*: {error}\n")
|
|
162
|
+
|
|
163
|
+
console.print(f"*Tokens*: {turn.tokens or 0} | *Cost*: ${turn.cost or 0.0:.4f}\n")
|
|
164
|
+
console.print("---\n")
|
|
165
|
+
|
|
166
|
+
else:
|
|
167
|
+
# Plain text output (default)
|
|
168
|
+
output = format_conversation_for_display(turns)
|
|
169
|
+
console.print(output)
|
|
170
|
+
|
|
171
|
+
except FileNotFoundError:
|
|
172
|
+
console.print(f"[red]Conversation '{conversation_id}' not found[/red]")
|
|
173
|
+
raise typer.Exit(1)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
console.print(f"[red]Failed to load conversation: {e}[/red]")
|
|
176
|
+
raise typer.Exit(1)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@history_app.command("rebuild-index")
|
|
180
|
+
def history_rebuild_index():
|
|
181
|
+
"""Rebuild conversation index from JSONL files.
|
|
182
|
+
|
|
183
|
+
Scans all conversation files and rebuilds the index.
|
|
184
|
+
Useful after manual file changes or index corruption.
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
console.print("Rebuilding conversation index...")
|
|
188
|
+
count = rebuild_index()
|
|
189
|
+
console.print(f"[green]✓ Indexed {count} conversations[/green]")
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
console.print(f"[red]Failed to rebuild index: {e}[/red]")
|
|
193
|
+
raise typer.Exit(1)
|
tsugite/cli/init.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Initialization command for first-time setup."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
import questionary
|
|
11
|
+
import typer
|
|
12
|
+
from prompt_toolkit.styles import Style
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.prompt import Confirm
|
|
16
|
+
|
|
17
|
+
from tsugite.config import get_config_path, load_config, save_config
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
# Custom questionary style for better readability
|
|
22
|
+
# Uses dark background for popup with light text for good contrast
|
|
23
|
+
questionary_style = Style(
|
|
24
|
+
[
|
|
25
|
+
("qmark", "fg:ansicyan bold"), # Question mark
|
|
26
|
+
("question", "bold"), # Question text
|
|
27
|
+
("answer", "fg:ansicyan bold"), # Selected answer
|
|
28
|
+
("pointer", "fg:ansicyan bold"), # Pointer arrow
|
|
29
|
+
("highlighted", "fg:ansiwhite bg:ansiblue"), # Highlighted choice - white on blue
|
|
30
|
+
("selected", "fg:ansicyan"), # Selected text
|
|
31
|
+
("separator", "fg:ansiblack"), # Separator
|
|
32
|
+
("instruction", ""), # Instructions
|
|
33
|
+
("text", ""), # Default text
|
|
34
|
+
# Autocomplete-specific styles
|
|
35
|
+
("completion-menu", "bg:ansiblack fg:ansiwhite"), # Popup background
|
|
36
|
+
("completion-menu.completion", "fg:ansiwhite bg:ansiblack"), # Individual items
|
|
37
|
+
("completion-menu.completion.current", "fg:ansiblack bg:ansicyan bold"), # Current selection - black on cyan
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@contextmanager
|
|
43
|
+
def suppress_litellm_warnings():
|
|
44
|
+
"""Temporarily suppress LiteLLM warnings."""
|
|
45
|
+
litellm_logger = logging.getLogger("LiteLLM")
|
|
46
|
+
original_level = litellm_logger.level
|
|
47
|
+
litellm_logger.setLevel(logging.ERROR)
|
|
48
|
+
try:
|
|
49
|
+
yield
|
|
50
|
+
finally:
|
|
51
|
+
litellm_logger.setLevel(original_level)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def detect_ollama() -> bool:
|
|
55
|
+
"""Check if Ollama is running."""
|
|
56
|
+
try:
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
["ollama", "list"],
|
|
59
|
+
capture_output=True,
|
|
60
|
+
timeout=2,
|
|
61
|
+
check=False,
|
|
62
|
+
)
|
|
63
|
+
return result.returncode == 0
|
|
64
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def fetch_provider_models(provider: str) -> List[str]:
|
|
69
|
+
"""Get available models for a provider using LiteLLM.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
provider: Provider name (ollama, openai, anthropic, google)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of model strings in LiteLLM format (e.g., "ollama/qwen2.5-coder:7b")
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
from litellm import get_valid_models
|
|
79
|
+
|
|
80
|
+
with suppress_litellm_warnings():
|
|
81
|
+
# For ollama, check endpoint to get locally installed models
|
|
82
|
+
if provider == "ollama":
|
|
83
|
+
models = get_valid_models(
|
|
84
|
+
custom_llm_provider="ollama",
|
|
85
|
+
check_provider_endpoint=True,
|
|
86
|
+
)
|
|
87
|
+
# For cloud providers, get models based on API key presence
|
|
88
|
+
elif provider == "openai":
|
|
89
|
+
models = get_valid_models(custom_llm_provider="openai")
|
|
90
|
+
elif provider == "anthropic":
|
|
91
|
+
models = get_valid_models(custom_llm_provider="anthropic")
|
|
92
|
+
elif provider == "google":
|
|
93
|
+
# Use "gemini" instead of "google" for LiteLLM
|
|
94
|
+
models = get_valid_models(custom_llm_provider="gemini")
|
|
95
|
+
# Convert gemini/* to google:* for consistency
|
|
96
|
+
models = [m.replace("gemini/", "google/") for m in models]
|
|
97
|
+
else:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
return models if models else []
|
|
101
|
+
except Exception:
|
|
102
|
+
# Silently return empty list on error
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def litellm_to_tsugite_format(model: str) -> str:
|
|
107
|
+
"""Convert LiteLLM model format to Tsugite format.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
model: Model in LiteLLM format (e.g., "ollama/qwen2.5-coder:7b")
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Model in Tsugite format (e.g., "ollama:qwen2.5-coder:7b")
|
|
114
|
+
"""
|
|
115
|
+
# Replace first / with :
|
|
116
|
+
return model.replace("/", ":", 1)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def detect_available_providers() -> dict[str, int]:
|
|
120
|
+
"""Detect which LLM providers are available and count their models.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dict of provider name -> model count (0 if unavailable)
|
|
124
|
+
"""
|
|
125
|
+
providers = {}
|
|
126
|
+
|
|
127
|
+
# Check each provider
|
|
128
|
+
for provider_name in ["ollama", "openai", "anthropic", "google"]:
|
|
129
|
+
models = fetch_provider_models(provider_name)
|
|
130
|
+
providers[provider_name] = len(models)
|
|
131
|
+
|
|
132
|
+
return providers
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def prompt_for_model(providers: dict[str, int], skip_prompt: bool = False, default_model: Optional[str] = None) -> str:
|
|
136
|
+
"""Prompt user to select a provider and then a model with arrow-key navigation.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
providers: Dict of provider name -> model count
|
|
140
|
+
skip_prompt: If True, auto-select first available provider and model
|
|
141
|
+
default_model: If provided, skip prompts entirely
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Model string in Tsugite format (e.g., "ollama:qwen2.5-coder:7b")
|
|
145
|
+
"""
|
|
146
|
+
if default_model:
|
|
147
|
+
return default_model
|
|
148
|
+
|
|
149
|
+
console.print("\n[bold cyan]Model Selection[/bold cyan]")
|
|
150
|
+
|
|
151
|
+
# Filter available providers
|
|
152
|
+
provider_choices = [(name, count) for name, count in providers.items() if count > 0]
|
|
153
|
+
|
|
154
|
+
if not provider_choices:
|
|
155
|
+
console.print("\n[yellow]No providers detected. You can still configure manually.[/yellow]")
|
|
156
|
+
if skip_prompt:
|
|
157
|
+
return "ollama:qwen2.5-coder:7b"
|
|
158
|
+
|
|
159
|
+
model = questionary.text(
|
|
160
|
+
"Enter model string (e.g., 'ollama:qwen2.5-coder:7b', 'openai:gpt-4o-mini'):",
|
|
161
|
+
default="ollama:qwen2.5-coder:7b",
|
|
162
|
+
style=questionary_style,
|
|
163
|
+
).ask()
|
|
164
|
+
return model or "ollama:qwen2.5-coder:7b"
|
|
165
|
+
|
|
166
|
+
# Loop to allow going back
|
|
167
|
+
while True:
|
|
168
|
+
# Step 1: Select Provider
|
|
169
|
+
if skip_prompt:
|
|
170
|
+
selected_provider = provider_choices[0][0]
|
|
171
|
+
else:
|
|
172
|
+
provider_options = [f"{name} ({count} models)" for name, count in provider_choices]
|
|
173
|
+
|
|
174
|
+
selected_display = questionary.select(
|
|
175
|
+
"Select a provider:",
|
|
176
|
+
choices=provider_options,
|
|
177
|
+
style=questionary_style,
|
|
178
|
+
).ask()
|
|
179
|
+
|
|
180
|
+
if not selected_display: # User cancelled (Ctrl+C)
|
|
181
|
+
raise KeyboardInterrupt
|
|
182
|
+
|
|
183
|
+
# Extract provider name from display string
|
|
184
|
+
selected_provider = selected_display.split(" (")[0]
|
|
185
|
+
|
|
186
|
+
# Step 2: Select Model from Provider
|
|
187
|
+
console.print(f"\n[bold cyan]Fetching {selected_provider} models...[/bold cyan]")
|
|
188
|
+
models = fetch_provider_models(selected_provider)
|
|
189
|
+
|
|
190
|
+
if not models:
|
|
191
|
+
console.print(f"[yellow]No models found for {selected_provider}[/yellow]")
|
|
192
|
+
if skip_prompt:
|
|
193
|
+
return f"{selected_provider}:default"
|
|
194
|
+
|
|
195
|
+
model = questionary.text(
|
|
196
|
+
f"Enter {selected_provider} model name:",
|
|
197
|
+
default="default",
|
|
198
|
+
style=questionary_style,
|
|
199
|
+
).ask()
|
|
200
|
+
return f"{selected_provider}:{model or 'default'}"
|
|
201
|
+
|
|
202
|
+
# Convert to Tsugite format
|
|
203
|
+
tsugite_models = [litellm_to_tsugite_format(m) for m in models]
|
|
204
|
+
|
|
205
|
+
if skip_prompt:
|
|
206
|
+
return tsugite_models[0]
|
|
207
|
+
|
|
208
|
+
# Build model choices with Back option at the end
|
|
209
|
+
special_choices = ["← Back to provider selection", "✎ Enter custom model name"]
|
|
210
|
+
model_choices = tsugite_models + special_choices
|
|
211
|
+
|
|
212
|
+
# Use autocomplete for searchable/filterable list
|
|
213
|
+
console.print("\n[dim]💡 Tip: Type to filter models, use arrow keys to navigate[/dim]")
|
|
214
|
+
selected_model = questionary.autocomplete(
|
|
215
|
+
f"Select a model from {selected_provider}:",
|
|
216
|
+
choices=model_choices,
|
|
217
|
+
match_middle=True, # Allow matching in middle of string
|
|
218
|
+
style=questionary_style,
|
|
219
|
+
).ask()
|
|
220
|
+
|
|
221
|
+
if not selected_model: # User cancelled
|
|
222
|
+
raise KeyboardInterrupt
|
|
223
|
+
|
|
224
|
+
if selected_model == "← Back to provider selection":
|
|
225
|
+
# Go back to provider selection
|
|
226
|
+
continue
|
|
227
|
+
elif selected_model == "✎ Enter custom model name":
|
|
228
|
+
# Custom model name
|
|
229
|
+
model_name = questionary.text(
|
|
230
|
+
f"Enter {selected_provider} model name:",
|
|
231
|
+
style=questionary_style,
|
|
232
|
+
).ask()
|
|
233
|
+
if model_name:
|
|
234
|
+
return f"{selected_provider}:{model_name}"
|
|
235
|
+
else:
|
|
236
|
+
continue # Go back if empty
|
|
237
|
+
else:
|
|
238
|
+
# Regular model selected
|
|
239
|
+
return selected_model
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def copy_template_agent(config_dir: Path) -> Path:
|
|
243
|
+
"""Copy template assistant agent to config directory."""
|
|
244
|
+
agents_dir = config_dir / "agents"
|
|
245
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
|
|
247
|
+
template_path = Path(__file__).parent.parent / "templates" / "assistant.md"
|
|
248
|
+
destination = agents_dir / "assistant.md"
|
|
249
|
+
|
|
250
|
+
if not destination.exists():
|
|
251
|
+
shutil.copy(template_path, destination)
|
|
252
|
+
console.print(f"[green]✓[/green] Created default agent: {destination}")
|
|
253
|
+
else:
|
|
254
|
+
console.print(f"[dim]Agent already exists: {destination}[/dim]")
|
|
255
|
+
|
|
256
|
+
return destination
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_setup(model: str, skip_test: bool = False) -> bool:
|
|
260
|
+
"""Run a quick test to verify setup works."""
|
|
261
|
+
if skip_test:
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
console.print("\n[bold cyan]Testing Setup[/bold cyan]")
|
|
265
|
+
|
|
266
|
+
if not Confirm.ask("Run a quick test?", default=True):
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
from io import StringIO
|
|
271
|
+
|
|
272
|
+
from rich.console import Console as RichConsole
|
|
273
|
+
|
|
274
|
+
from tsugite.agent_runner import run_agent
|
|
275
|
+
from tsugite.ui import custom_agent_ui
|
|
276
|
+
|
|
277
|
+
# Find the assistant agent
|
|
278
|
+
config_dir = get_config_path().parent
|
|
279
|
+
agent_path = config_dir / "agents" / "assistant.md"
|
|
280
|
+
|
|
281
|
+
if not agent_path.exists():
|
|
282
|
+
console.print("[yellow]Warning: Assistant agent not found, skipping test[/yellow]")
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
console.print("[dim]Running: tsugite run +assistant 'say hello'[/dim]")
|
|
286
|
+
|
|
287
|
+
# Create a quiet logger that doesn't output anything
|
|
288
|
+
quiet_console = RichConsole(file=StringIO())
|
|
289
|
+
with custom_agent_ui(
|
|
290
|
+
quiet_console,
|
|
291
|
+
show_code=False,
|
|
292
|
+
show_observations=False,
|
|
293
|
+
show_progress=False,
|
|
294
|
+
show_llm_messages=False,
|
|
295
|
+
show_execution_results=False,
|
|
296
|
+
show_execution_logs=False,
|
|
297
|
+
show_panels=False,
|
|
298
|
+
) as logger:
|
|
299
|
+
result = run_agent(
|
|
300
|
+
agent_path=agent_path,
|
|
301
|
+
prompt="Say hello in one sentence",
|
|
302
|
+
model_override=model,
|
|
303
|
+
debug=False,
|
|
304
|
+
custom_logger=logger,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
console.print("[green]✓[/green] Test successful!")
|
|
308
|
+
console.print(
|
|
309
|
+
f"[dim]Response: {result[:100]}...[/dim]" if len(result) > 100 else f"[dim]Response: {result}[/dim]"
|
|
310
|
+
)
|
|
311
|
+
return True
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
console.print(f"[yellow]Warning: Test failed: {e}[/yellow]")
|
|
315
|
+
console.print("[dim]You can still use tsugite, but you may need to configure your model.[/dim]")
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def init(
|
|
320
|
+
model: Optional[str] = typer.Option(None, "--model", help="Model to use (skip prompt)"),
|
|
321
|
+
force: bool = typer.Option(False, "--force", help="Overwrite existing configuration"),
|
|
322
|
+
):
|
|
323
|
+
"""Initialize tsugite with global configuration."""
|
|
324
|
+
from tsugite.cli.helpers import get_logo
|
|
325
|
+
|
|
326
|
+
# Show welcome
|
|
327
|
+
console.print(get_logo(console), style="cyan")
|
|
328
|
+
console.print()
|
|
329
|
+
console.print(
|
|
330
|
+
Panel(
|
|
331
|
+
"[bold]Welcome to Tsugite![/bold]\n\n"
|
|
332
|
+
"This wizard will set up your global configuration.\n"
|
|
333
|
+
"You can reconfigure anytime with: [cyan]tsugite config[/cyan]",
|
|
334
|
+
border_style="blue",
|
|
335
|
+
title="First-Time Setup",
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Check if already configured
|
|
340
|
+
config_path = get_config_path()
|
|
341
|
+
config = load_config()
|
|
342
|
+
|
|
343
|
+
if config_path.exists() and config.default_model and not force:
|
|
344
|
+
console.print(f"\n[yellow]Configuration already exists at: {config_path}[/yellow]")
|
|
345
|
+
console.print(f"Current default model: [cyan]{config.default_model}[/cyan]")
|
|
346
|
+
|
|
347
|
+
if not Confirm.ask("Reconfigure?", default=False):
|
|
348
|
+
console.print("\n[dim]Setup cancelled. Use --force to overwrite.[/dim]")
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
# Detect providers
|
|
352
|
+
console.print("\n[bold cyan]Detecting LLM Providers[/bold cyan]")
|
|
353
|
+
providers = detect_available_providers()
|
|
354
|
+
|
|
355
|
+
# Prompt for model
|
|
356
|
+
selected_model = prompt_for_model(providers, skip_prompt=bool(model), default_model=model)
|
|
357
|
+
|
|
358
|
+
# Create config
|
|
359
|
+
console.print("\n[bold cyan]Creating Configuration[/bold cyan]")
|
|
360
|
+
config.default_model = selected_model
|
|
361
|
+
save_config(config)
|
|
362
|
+
|
|
363
|
+
console.print(f"[green]✓[/green] Configuration saved to: [dim]{config_path}[/dim]")
|
|
364
|
+
console.print(f"[green]✓[/green] Default model: [cyan]{selected_model}[/cyan]")
|
|
365
|
+
|
|
366
|
+
# Copy template agent
|
|
367
|
+
console.print("\n[bold cyan]Setting Up Default Agent[/bold cyan]")
|
|
368
|
+
config_dir = config_path.parent
|
|
369
|
+
agent_path = copy_template_agent(config_dir)
|
|
370
|
+
|
|
371
|
+
# Show next steps
|
|
372
|
+
console.print("\n" + "=" * 60)
|
|
373
|
+
console.print("[bold green]Setup Complete![/bold green]")
|
|
374
|
+
console.print("=" * 60)
|
|
375
|
+
|
|
376
|
+
console.print("\n[bold]Next Steps:[/bold]")
|
|
377
|
+
console.print(" 1. Run the assistant: [cyan]tsugite run +assistant 'your task'[/cyan]")
|
|
378
|
+
console.print(" 2. View configuration: [cyan]tsugite config show[/cyan]")
|
|
379
|
+
console.print(" 3. Explore examples: [cyan]tsugite agents list[/cyan]")
|
|
380
|
+
console.print(f" 4. Customize your agent: [cyan]{agent_path}[/cyan]")
|
|
381
|
+
|
|
382
|
+
console.print("\n[bold]Documentation:[/bold]")
|
|
383
|
+
console.print(" • Quick start: See README.md")
|
|
384
|
+
console.print(" • Agent guide: See CLAUDE.md")
|
|
385
|
+
console.print(" • Examples: See examples/ directory")
|
|
386
|
+
|
|
387
|
+
console.print()
|