devpilot-agentic-cli 1.0.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.
- agent/__init__.py +1 -0
- agent/a2a_client.py +94 -0
- agent/a2a_server.py +148 -0
- agent/cli.py +233 -0
- agent/config.py +232 -0
- agent/context.py +182 -0
- agent/history.py +172 -0
- agent/loop.py +102 -0
- agent/mcp_client.py +104 -0
- agent/providers/__init__.py +4 -0
- agent/providers/anthropic_provider.py +169 -0
- agent/providers/base.py +148 -0
- agent/providers/factory.py +35 -0
- agent/providers/openai_provider.py +194 -0
- agent/providers/system_prompt.py +132 -0
- agent/setup_wizard.py +309 -0
- agent/tools/__init__.py +15 -0
- agent/tools/a2a.py +56 -0
- agent/tools/base.py +52 -0
- agent/tools/diagram.py +131 -0
- agent/tools/doc_gen.py +163 -0
- agent/tools/fs.py +411 -0
- agent/tools/git_ops.py +145 -0
- agent/tools/registry.py +219 -0
- agent/tools/search_code.py +120 -0
- agent/tools/shell.py +118 -0
- agent/tools/web_search.py +105 -0
- agent/tui/__init__.py +3 -0
- agent/tui/app.py +557 -0
- agent/ui.py +263 -0
- devpilot_agentic_cli-1.0.0.dist-info/METADATA +288 -0
- devpilot_agentic_cli-1.0.0.dist-info/RECORD +35 -0
- devpilot_agentic_cli-1.0.0.dist-info/WHEEL +5 -0
- devpilot_agentic_cli-1.0.0.dist-info/entry_points.txt +2 -0
- devpilot_agentic_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent/providers/system_prompt.py
|
|
3
|
+
──────────────────────────────────
|
|
4
|
+
Single source of truth for DevPilot's core system prompt.
|
|
5
|
+
|
|
6
|
+
Both AnthropicProvider and OpenAIProvider import `build_system_prompt()`
|
|
7
|
+
from here so the prompt is never duplicated.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import platform
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_system_prompt(repo_context_block: str = "") -> str:
|
|
16
|
+
"""
|
|
17
|
+
Build the full DevPilot system prompt.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
repo_context_block: Output of RepoContext.build_context_block(),
|
|
21
|
+
injected at every call so the model always
|
|
22
|
+
knows what it has already read this session.
|
|
23
|
+
"""
|
|
24
|
+
if platform.system() == "Windows":
|
|
25
|
+
shell_rules = """\
|
|
26
|
+
### RULES FOR SHELL COMMANDS
|
|
27
|
+
- You are running on **Windows with PowerShell**.
|
|
28
|
+
- For finding files: `Get-ChildItem -Recurse -Filter *.py`
|
|
29
|
+
- For searching text: `Select-String -Path . -Recurse -Pattern "keyword"`
|
|
30
|
+
- For running scripts: `python script.py` (never `./script.py`)
|
|
31
|
+
- Chain commands with `;` not `&&`."""
|
|
32
|
+
else:
|
|
33
|
+
shell_rules = """\
|
|
34
|
+
### RULES FOR SHELL COMMANDS
|
|
35
|
+
- You are running on **Linux / macOS with bash/zsh**.
|
|
36
|
+
- For finding files: `find . -name "*.py"` or `fd -e py`
|
|
37
|
+
- For searching text: `grep -r "keyword" .` or `rg "keyword"`
|
|
38
|
+
- For running scripts: `python script.py` or `./script.py`
|
|
39
|
+
- Chain commands with `&&`."""
|
|
40
|
+
|
|
41
|
+
context_section = ""
|
|
42
|
+
if repo_context_block.strip():
|
|
43
|
+
context_section = f"""\
|
|
44
|
+
|
|
45
|
+
### CURRENT SESSION CONTEXT
|
|
46
|
+
The following is automatically maintained by DevPilot. It shows every file
|
|
47
|
+
you have already read this session and a snapshot of the project structure.
|
|
48
|
+
Use it to avoid redundant reads. Files marked ⚠ have changed on disk since
|
|
49
|
+
you last read them — re-read before editing.
|
|
50
|
+
|
|
51
|
+
{repo_context_block}
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
return f"""\
|
|
55
|
+
You are DevPilot, an elite autonomous AI software engineer running directly in
|
|
56
|
+
the user's terminal. Your goal is to solve complex engineering tasks autonomously
|
|
57
|
+
while maintaining absolute code integrity.
|
|
58
|
+
|
|
59
|
+
You have a powerful suite of tools: read_file, write_file, edit_file, list_files,
|
|
60
|
+
run_bash, search_code, git_status, git_commit, and more.
|
|
61
|
+
|
|
62
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
63
|
+
CORE METHODOLOGY — PLAN → ACT → VERIFY
|
|
64
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
65
|
+
|
|
66
|
+
Follow this methodology for every non-trivial task:
|
|
67
|
+
|
|
68
|
+
0. RECALL
|
|
69
|
+
At the start of every session, check if `.devpilot/memory.md` exists using
|
|
70
|
+
read_file. If it does, absorb its architectural notes before proceeding.
|
|
71
|
+
|
|
72
|
+
1. EXPLORE
|
|
73
|
+
Never guess file paths, variable names, or architecture.
|
|
74
|
+
- Check the SESSION CONTEXT block below first — you may already have the file.
|
|
75
|
+
- Use list_files or search_code to locate what you need.
|
|
76
|
+
- Use read_file to understand exact context before editing.
|
|
77
|
+
|
|
78
|
+
2. PLAN
|
|
79
|
+
For tasks spanning multiple files, write your plan to `.devpilot/memory.md`
|
|
80
|
+
using write_file before acting. This persists your thinking across sessions.
|
|
81
|
+
Use a <thinking> block for shorter in-line reasoning.
|
|
82
|
+
|
|
83
|
+
3. ACT
|
|
84
|
+
Execute your plan step by step:
|
|
85
|
+
- Use edit_file for targeted replacements in existing files (preferred).
|
|
86
|
+
- Use write_file only for new files or complete rewrites.
|
|
87
|
+
- Never use placeholders like "# ... existing code ..." in write_file output.
|
|
88
|
+
You must always write the ENTIRE file, every line, without omission.
|
|
89
|
+
|
|
90
|
+
4. VERIFY
|
|
91
|
+
After every code change, run tests, a linter, or a compile command via
|
|
92
|
+
run_bash. Do not assume your code works. Report the result to the user.
|
|
93
|
+
|
|
94
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
95
|
+
RULES FOR EDITING CODE
|
|
96
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
97
|
+
|
|
98
|
+
- ALWAYS call read_file before edit_file. The old_content parameter must be an
|
|
99
|
+
exact, character-for-character match including all whitespace and indentation.
|
|
100
|
+
If old_content doesn't match, the edit is rejected — read first, always.
|
|
101
|
+
- edit_file is preferred over write_file for existing files. It is token-efficient
|
|
102
|
+
and surgically precise; write_file rewrites the entire file and wastes context.
|
|
103
|
+
- When write_file is unavoidable, output the COMPLETE file. Not a summary.
|
|
104
|
+
Not a stub. Every. Single. Line.
|
|
105
|
+
|
|
106
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
107
|
+
TOOL SELECTION GUIDE
|
|
108
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
109
|
+
|
|
110
|
+
Explore structure → list_files (recursive=true)
|
|
111
|
+
Find exact text in code → search_code (faster than grep for source)
|
|
112
|
+
|
|
113
|
+
Find files by name → run_bash with find / Get-ChildItem
|
|
114
|
+
Targeted code replacement → edit_file ← PREFERRED for existing files
|
|
115
|
+
Create a new file → write_file
|
|
116
|
+
Run tests / linter / build → run_bash (pytest, tsc, eslint, cargo test…)
|
|
117
|
+
Check uncommitted changes → git_status
|
|
118
|
+
Commit completed work → git_commit (surgical staging, not git add .)
|
|
119
|
+
Remember across sessions → write_file → .devpilot/memory.md
|
|
120
|
+
|
|
121
|
+
{shell_rules}
|
|
122
|
+
|
|
123
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
124
|
+
COMMUNICATION
|
|
125
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
126
|
+
|
|
127
|
+
- Be concise. Developers do not want paragraphs of fluff.
|
|
128
|
+
- Before calling a tool, state in one sentence what you are about to do and why.
|
|
129
|
+
- After completing a task (including verification), give a brief summary of
|
|
130
|
+
what changed and what was verified.
|
|
131
|
+
- If a task is ambiguous, ask one clarifying question before acting.
|
|
132
|
+
{context_section}"""
|
agent/setup_wizard.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent/setup_wizard.py
|
|
3
|
+
──────────────────────
|
|
4
|
+
First-run setup wizard for DevPilot.
|
|
5
|
+
|
|
6
|
+
Runs when no API key is detected. Guides the user through:
|
|
7
|
+
1. Choosing a provider (Anthropic, OpenAI, or custom compatible)
|
|
8
|
+
2. Entering their API key
|
|
9
|
+
3. Optionally choosing a model
|
|
10
|
+
4. Saving everything to a .env file in the current directory
|
|
11
|
+
|
|
12
|
+
Completely non-interactive when running in CI (no TTY).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from rich.prompt import Confirm, Prompt
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
# ── Model lists ───────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
_ANTHROPIC_MODELS = [
|
|
31
|
+
("claude-opus-4-5-20251101", "Most capable — best for complex tasks"),
|
|
32
|
+
("claude-sonnet-4-5-20251101", "Balanced — fast and capable"),
|
|
33
|
+
("claude-haiku-4-5-20251101", "Fastest — best for simple tasks"),
|
|
34
|
+
("claude-3-7-sonnet-20250219", "Extended thinking support"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
_OPENAI_MODELS = [
|
|
38
|
+
("gpt-4o", "Latest GPT-4o — best for coding"),
|
|
39
|
+
("gpt-4o-mini", "Fast and cheap — good for simple tasks"),
|
|
40
|
+
("o3", "Most powerful reasoning model"),
|
|
41
|
+
("o4-mini", "Fast reasoning — great for code"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# OpenAI-compatible third-party providers
|
|
45
|
+
# (base_url, display_name, key_env_var, key_url, models)
|
|
46
|
+
_COMPATIBLE_PROVIDERS = {
|
|
47
|
+
"1": (
|
|
48
|
+
"Groq",
|
|
49
|
+
"https://console.groq.com/keys",
|
|
50
|
+
"GROQ_API_KEY",
|
|
51
|
+
"https://api.groq.com/openai/v1",
|
|
52
|
+
[
|
|
53
|
+
("llama-3.3-70b-versatile", "Best Groq model — fast & capable"),
|
|
54
|
+
("llama-3.1-8b-instant", "Ultra-fast, lightweight"),
|
|
55
|
+
("mixtral-8x7b-32768", "Large context window"),
|
|
56
|
+
("gemma2-9b-it", "Google Gemma 2 — fast"),
|
|
57
|
+
],
|
|
58
|
+
),
|
|
59
|
+
"2": (
|
|
60
|
+
"Together AI",
|
|
61
|
+
"https://api.together.xyz/settings/api-keys",
|
|
62
|
+
"TOGETHER_API_KEY",
|
|
63
|
+
"https://api.together.xyz/v1",
|
|
64
|
+
[
|
|
65
|
+
("meta-llama/Llama-3-70b-chat-hf", "Llama 3 70B — best quality"),
|
|
66
|
+
("meta-llama/Llama-3-8b-chat-hf", "Llama 3 8B — faster"),
|
|
67
|
+
("mistralai/Mixtral-8x7B-v0.1", "Mixtral — large context"),
|
|
68
|
+
],
|
|
69
|
+
),
|
|
70
|
+
"3": (
|
|
71
|
+
"Mistral AI",
|
|
72
|
+
"https://console.mistral.ai/api-keys/",
|
|
73
|
+
"MISTRAL_API_KEY",
|
|
74
|
+
"https://api.mistral.ai/v1",
|
|
75
|
+
[
|
|
76
|
+
("mistral-large-latest", "Most capable Mistral model"),
|
|
77
|
+
("mistral-small-latest", "Fast and affordable"),
|
|
78
|
+
("codestral-latest", "Optimised for code"),
|
|
79
|
+
],
|
|
80
|
+
),
|
|
81
|
+
"4": (
|
|
82
|
+
"Ollama (local)",
|
|
83
|
+
"https://ollama.com/library",
|
|
84
|
+
"OLLAMA_API_KEY", # Ollama doesn't need a real key
|
|
85
|
+
"http://localhost:11434/v1",
|
|
86
|
+
[
|
|
87
|
+
("qwen2.5-coder:7b", "Best local coding model (recommended)"),
|
|
88
|
+
("codellama:13b", "Meta CodeLlama 13B"),
|
|
89
|
+
("deepseek-coder:6b", "DeepSeek Coder 6B"),
|
|
90
|
+
("llama3.2:3b", "Llama 3.2 3B — very fast"),
|
|
91
|
+
],
|
|
92
|
+
),
|
|
93
|
+
"5": (
|
|
94
|
+
"Other (custom)",
|
|
95
|
+
"",
|
|
96
|
+
"",
|
|
97
|
+
"",
|
|
98
|
+
[],
|
|
99
|
+
),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
def _is_interactive() -> bool:
|
|
106
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _write_env_file(
|
|
110
|
+
path: Path,
|
|
111
|
+
updates: dict[str, str],
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Write or update .env file preserving existing entries."""
|
|
114
|
+
existing: dict[str, str] = {}
|
|
115
|
+
if path.exists():
|
|
116
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
117
|
+
line = line.strip()
|
|
118
|
+
if line and not line.startswith("#") and "=" in line:
|
|
119
|
+
k, _, v = line.partition("=")
|
|
120
|
+
existing[k.strip()] = v.strip()
|
|
121
|
+
|
|
122
|
+
existing.update(updates)
|
|
123
|
+
|
|
124
|
+
lines = [
|
|
125
|
+
"# DevPilot configuration — generated by setup wizard",
|
|
126
|
+
"# Add this file to .gitignore — never commit API keys!\n",
|
|
127
|
+
]
|
|
128
|
+
for k, v in existing.items():
|
|
129
|
+
lines.append(f"{k}={v}")
|
|
130
|
+
|
|
131
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _pick_model(models: list[tuple[str, str]], allow_custom: bool = False) -> str:
|
|
135
|
+
"""Show a model selection table and return the chosen model string."""
|
|
136
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
137
|
+
for i, (name, desc) in enumerate(models, 1):
|
|
138
|
+
table.add_row(
|
|
139
|
+
f"[cyan]{i}[/cyan]",
|
|
140
|
+
f"[bold]{name}[/bold]",
|
|
141
|
+
f"[dim]{desc}[/dim]",
|
|
142
|
+
)
|
|
143
|
+
if allow_custom:
|
|
144
|
+
table.add_row(
|
|
145
|
+
f"[cyan]{len(models)+1}[/cyan]",
|
|
146
|
+
"[bold]Other[/bold]",
|
|
147
|
+
"[dim]Enter a custom model name[/dim]",
|
|
148
|
+
)
|
|
149
|
+
console.print(table)
|
|
150
|
+
|
|
151
|
+
choices = [str(i) for i in range(1, len(models) + (2 if allow_custom else 1))]
|
|
152
|
+
choice = Prompt.ask("\nModel", choices=choices, default="1")
|
|
153
|
+
|
|
154
|
+
if allow_custom and choice == str(len(models) + 1):
|
|
155
|
+
return Prompt.ask("Enter model name").strip()
|
|
156
|
+
|
|
157
|
+
return models[int(choice) - 1][0]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── Main wizard ───────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
def run_setup_wizard(env_path: Path | None = None) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Run the interactive first-run setup wizard.
|
|
165
|
+
Returns True if setup completed, False if skipped/failed.
|
|
166
|
+
"""
|
|
167
|
+
if not _is_interactive():
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
env_path = env_path or Path(".env")
|
|
171
|
+
|
|
172
|
+
console.print(Panel(
|
|
173
|
+
"[bold cyan]Welcome to DevPilot! 🚀[/bold cyan]\n\n"
|
|
174
|
+
"No API key was found. Let's set up your configuration.\n"
|
|
175
|
+
"This will create a [bold].env[/bold] file in your current directory.",
|
|
176
|
+
border_style="cyan",
|
|
177
|
+
expand=False,
|
|
178
|
+
))
|
|
179
|
+
|
|
180
|
+
# ── Step 1: Choose provider ───────────────────────────────────────────────
|
|
181
|
+
console.print("\n[bold]Step 1 of 3 — Choose your AI provider[/bold]\n")
|
|
182
|
+
console.print(" [cyan]1[/cyan] Anthropic (Claude — recommended)")
|
|
183
|
+
console.print(" [cyan]2[/cyan] OpenAI (GPT-4o, o3, o4-mini)")
|
|
184
|
+
console.print(" [cyan]3[/cyan] Groq (Llama 3, Mixtral — very fast, free tier)")
|
|
185
|
+
console.print(" [cyan]4[/cyan] Together AI (Llama 3, Mixtral)")
|
|
186
|
+
console.print(" [cyan]5[/cyan] Mistral AI (Mistral, Codestral)")
|
|
187
|
+
console.print(" [cyan]6[/cyan] Ollama (local models, no API key needed)")
|
|
188
|
+
console.print(" [cyan]7[/cyan] Other (any OpenAI-compatible endpoint)")
|
|
189
|
+
|
|
190
|
+
choice = Prompt.ask("\nProvider", choices=["1","2","3","4","5","6","7"], default="1")
|
|
191
|
+
|
|
192
|
+
env_updates: dict[str, str] = {}
|
|
193
|
+
|
|
194
|
+
# ── Anthropic ─────────────────────────────────────────────────────────────
|
|
195
|
+
if choice == "1":
|
|
196
|
+
console.print("\n[bold]Step 2 of 3 — Enter your Anthropic API key[/bold]")
|
|
197
|
+
console.print(" Get one at: [link=https://console.anthropic.com/]https://console.anthropic.com/[/link]")
|
|
198
|
+
api_key = Prompt.ask("\nANTHROPIC_API_KEY", password=True).strip()
|
|
199
|
+
if not api_key:
|
|
200
|
+
console.print("[red]API key cannot be empty.[/red]")
|
|
201
|
+
return False
|
|
202
|
+
if not api_key.startswith("sk-ant-"):
|
|
203
|
+
console.print("[yellow]⚠ Key doesn't look like a typical Anthropic key (sk-ant-...). Saving anyway.[/yellow]")
|
|
204
|
+
|
|
205
|
+
console.print("\n[bold]Step 3 of 3 — Choose a model[/bold]\n")
|
|
206
|
+
model = _pick_model(_ANTHROPIC_MODELS)
|
|
207
|
+
|
|
208
|
+
env_updates = {
|
|
209
|
+
"ANTHROPIC_API_KEY": api_key,
|
|
210
|
+
"DEVPILOT_PROVIDER": "anthropic",
|
|
211
|
+
"DEVPILOT_MODEL": model,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# ── OpenAI ────────────────────────────────────────────────────────────────
|
|
215
|
+
elif choice == "2":
|
|
216
|
+
console.print("\n[bold]Step 2 of 3 — Enter your OpenAI API key[/bold]")
|
|
217
|
+
console.print(" Get one at: [link=https://platform.openai.com/api-keys]https://platform.openai.com/api-keys[/link]")
|
|
218
|
+
api_key = Prompt.ask("\nOPENAI_API_KEY", password=True).strip()
|
|
219
|
+
if not api_key:
|
|
220
|
+
console.print("[red]API key cannot be empty.[/red]")
|
|
221
|
+
return False
|
|
222
|
+
if not api_key.startswith("sk-"):
|
|
223
|
+
console.print("[yellow]⚠ Key doesn't look like a typical OpenAI key (sk-...). Saving anyway.[/yellow]")
|
|
224
|
+
|
|
225
|
+
console.print("\n[bold]Step 3 of 3 — Choose a model[/bold]\n")
|
|
226
|
+
model = _pick_model(_OPENAI_MODELS, allow_custom=True)
|
|
227
|
+
|
|
228
|
+
env_updates = {
|
|
229
|
+
"OPENAI_API_KEY": api_key,
|
|
230
|
+
"DEVPILOT_PROVIDER": "openai",
|
|
231
|
+
"DEVPILOT_MODEL": model,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# ── Compatible providers (Groq, Together, Mistral, Ollama) ───────────────
|
|
235
|
+
elif choice in ("3", "4", "5", "6"):
|
|
236
|
+
compat_key = str(int(choice) - 2) # maps 3→1, 4→2, 5→3, 6→4
|
|
237
|
+
name, key_url, key_env, base_url, models = _COMPATIBLE_PROVIDERS[compat_key]
|
|
238
|
+
|
|
239
|
+
console.print(f"\n[bold]Step 2 of 3 — Enter your {name} API key[/bold]")
|
|
240
|
+
|
|
241
|
+
if choice == "6": # Ollama — no real key needed
|
|
242
|
+
console.print(" [dim]Ollama runs locally — no API key required.[/dim]")
|
|
243
|
+
console.print(" Make sure Ollama is running: [bold]ollama serve[/bold]")
|
|
244
|
+
api_key = "ollama"
|
|
245
|
+
else:
|
|
246
|
+
console.print(f" Get one at: [link={key_url}]{key_url}[/link]")
|
|
247
|
+
api_key = Prompt.ask(f"\n{key_env}", password=True).strip()
|
|
248
|
+
if not api_key:
|
|
249
|
+
console.print("[red]API key cannot be empty.[/red]")
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
console.print(f"\n[bold]Step 3 of 3 — Choose a model[/bold]\n")
|
|
253
|
+
model = _pick_model(models, allow_custom=True)
|
|
254
|
+
|
|
255
|
+
env_updates = {
|
|
256
|
+
key_env: api_key,
|
|
257
|
+
"OPENAI_API_KEY": api_key, # DevPilot reads OPENAI_API_KEY for openai provider
|
|
258
|
+
"DEVPILOT_PROVIDER": "openai",
|
|
259
|
+
"DEVPILOT_MODEL": model,
|
|
260
|
+
"DEVPILOT_BASE_URL": base_url,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# ── Custom endpoint ───────────────────────────────────────────────────────
|
|
264
|
+
elif choice == "7":
|
|
265
|
+
console.print("\n[bold]Step 2 of 3 — Custom OpenAI-compatible endpoint[/bold]")
|
|
266
|
+
base_url = Prompt.ask("Base URL (e.g. https://api.example.com/v1)").strip()
|
|
267
|
+
api_key = Prompt.ask("API key", password=True).strip()
|
|
268
|
+
model = Prompt.ask("Model name (e.g. llama-3-70b)").strip()
|
|
269
|
+
|
|
270
|
+
if not base_url or not model:
|
|
271
|
+
console.print("[red]Base URL and model name are required.[/red]")
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
env_updates = {
|
|
275
|
+
"OPENAI_API_KEY": api_key or "none",
|
|
276
|
+
"DEVPILOT_PROVIDER": "openai",
|
|
277
|
+
"DEVPILOT_MODEL": model,
|
|
278
|
+
"DEVPILOT_BASE_URL": base_url,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# ── Save .env ─────────────────────────────────────────────────────────────
|
|
282
|
+
try:
|
|
283
|
+
_write_env_file(env_path, env_updates)
|
|
284
|
+
except OSError as e:
|
|
285
|
+
console.print(f"[red]Failed to write .env file: {e}[/red]")
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
# Inject into current process immediately
|
|
289
|
+
for k, v in env_updates.items():
|
|
290
|
+
os.environ[k] = v
|
|
291
|
+
|
|
292
|
+
provider_display = env_updates.get("DEVPILOT_PROVIDER", "openai")
|
|
293
|
+
model_display = env_updates.get("DEVPILOT_MODEL", "")
|
|
294
|
+
base_url_display = env_updates.get("DEVPILOT_BASE_URL", "")
|
|
295
|
+
|
|
296
|
+
summary = (
|
|
297
|
+
f"[bold green]✓ Setup complete![/bold green]\n\n"
|
|
298
|
+
f" Provider : [cyan]{provider_display}[/cyan]\n"
|
|
299
|
+
f" Model : [cyan]{model_display}[/cyan]\n"
|
|
300
|
+
)
|
|
301
|
+
if base_url_display:
|
|
302
|
+
summary += f" Base URL : [cyan]{base_url_display}[/cyan]\n"
|
|
303
|
+
summary += (
|
|
304
|
+
f" Saved to : [cyan]{env_path.resolve()}[/cyan]\n\n"
|
|
305
|
+
"[dim]Add .env to your .gitignore — never commit API keys![/dim]"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
console.print(Panel(summary, border_style="green", expand=False))
|
|
309
|
+
return True
|
agent/tools/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent/tools package.
|
|
3
|
+
Contains all built-in tools and the ToolRegistry.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from agent.tools.base import ToolResult, ToolSchema, BaseTool
|
|
7
|
+
from agent.tools.registry import ToolRegistry, PermissionGuard
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ToolResult",
|
|
11
|
+
"ToolSchema",
|
|
12
|
+
"BaseTool",
|
|
13
|
+
"ToolRegistry",
|
|
14
|
+
"PermissionGuard",
|
|
15
|
+
]
|
agent/tools/a2a.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent/tools/a2a.py
|
|
3
|
+
──────────────────
|
|
4
|
+
A2A delegation tool.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from agent.a2a_client import delegate_task_to_peer
|
|
12
|
+
from agent.tools.base import BaseTool, ToolResult, ToolSchema
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from agent.config import Config
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class A2ATool(BaseTool):
|
|
19
|
+
"""Delegate tasks to peer agents."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config: "Config") -> None:
|
|
22
|
+
self._config = config
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def schema(self) -> ToolSchema:
|
|
26
|
+
return ToolSchema(
|
|
27
|
+
name="a2a_delegate_task",
|
|
28
|
+
description=(
|
|
29
|
+
"Delegate a subtask to an external A2A (Agent-to-Agent) peer. "
|
|
30
|
+
"Use this when you need help from a specialist agent or another node. "
|
|
31
|
+
"Provide the base URL of the peer agent and the task prompt."
|
|
32
|
+
),
|
|
33
|
+
parameters={
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"peer_url": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "Base URL of the peer agent (e.g., http://localhost:8001)"
|
|
39
|
+
},
|
|
40
|
+
"prompt": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "The coding task or instruction to delegate."
|
|
43
|
+
},
|
|
44
|
+
"token": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Optional Bearer token if the peer requires authentication."
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"required": ["peer_url", "prompt"]
|
|
50
|
+
},
|
|
51
|
+
required=["peer_url", "prompt"],
|
|
52
|
+
sprint="Sprint 4",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def execute(self, peer_url: str, prompt: str, token: str | None = None) -> ToolResult: # type: ignore[override]
|
|
56
|
+
return await delegate_task_to_peer(peer_url, prompt, token)
|
agent/tools/base.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent/tools/base.py
|
|
3
|
+
───────────────────
|
|
4
|
+
Base interfaces for DevPilot tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ToolResult:
|
|
15
|
+
"""Returned by every tool executor."""
|
|
16
|
+
output: str # String content to send back to the model
|
|
17
|
+
is_error: bool # True if the tool raised an error or was cancelled
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ToolSchema:
|
|
22
|
+
"""Portable tool schema — provider-agnostic."""
|
|
23
|
+
name: str
|
|
24
|
+
description: str
|
|
25
|
+
parameters: dict[str, Any] # JSON Schema object for input
|
|
26
|
+
required: list[str] = field(default_factory=list)
|
|
27
|
+
is_destructive: bool = False # If True, permission guard prompts
|
|
28
|
+
sprint: str = "Sprint 1" # Implemented in which sprint
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseTool(ABC):
|
|
32
|
+
"""Abstract base class for all DevPilot tools."""
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def schema(self) -> ToolSchema:
|
|
37
|
+
"""Return the tool's JSON schema for the model."""
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
41
|
+
"""
|
|
42
|
+
Execute the tool with the arguments the model provided.
|
|
43
|
+
Returns a ToolResult containing output and error status.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def name(self) -> str:
|
|
48
|
+
return self.schema.name
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_destructive(self) -> bool:
|
|
52
|
+
return self.schema.is_destructive
|