sky-dev 0.0.5__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.
- sky/__init__.py +6 -0
- sky/cli.py +833 -0
- sky/config/__init__.py +19 -0
- sky/config/models.yaml +39 -0
- sky/config/schema.py +275 -0
- sky/core/__init__.py +48 -0
- sky/core/approval.py +92 -0
- sky/core/benchmark.py +39 -0
- sky/core/chat.py +211 -0
- sky/core/fast_loop.py +330 -0
- sky/core/mode_prompts.py +106 -0
- sky/core/router.py +264 -0
- sky/core/subagent.py +302 -0
- sky/core/workflow.py +662 -0
- sky/errors.py +29 -0
- sky/memory/__init__.py +27 -0
- sky/memory/indexer.py +450 -0
- sky/memory/vectorstore.py +271 -0
- sky/security/__init__.py +17 -0
- sky/security/audit.py +23 -0
- sky/security/detection.py +94 -0
- sky/security/guardrails.py +106 -0
- sky/security/prompts.py +58 -0
- sky/security/rate_limit.py +36 -0
- sky/security/sanitize.py +152 -0
- sky/storage/__init__.py +21 -0
- sky/storage/db.py +492 -0
- sky/tools/__init__.py +26 -0
- sky/tools/fs_tools.py +139 -0
- sky/tools/git_tools.py +139 -0
- sky/tools/registry.py +219 -0
- sky/tools/search_tools.py +128 -0
- sky/tools/shell_tools.py +73 -0
- sky_dev-0.0.5.dist-info/METADATA +119 -0
- sky_dev-0.0.5.dist-info/RECORD +39 -0
- sky_dev-0.0.5.dist-info/WHEEL +5 -0
- sky_dev-0.0.5.dist-info/entry_points.txt +2 -0
- sky_dev-0.0.5.dist-info/licenses/LICENSE +21 -0
- sky_dev-0.0.5.dist-info/top_level.txt +1 -0
sky/core/chat.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Chat mode for conversational interaction with Sky."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.markdown import Markdown
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.prompt import Confirm, Prompt
|
|
8
|
+
|
|
9
|
+
from sky.config import load_config, load_models_config
|
|
10
|
+
from sky.core.router import ModelRouter
|
|
11
|
+
from sky.storage import get_db
|
|
12
|
+
from sky.core.fast_loop import FastLoopEngine
|
|
13
|
+
from sky.core.mode_prompts import get_mode_prompt
|
|
14
|
+
|
|
15
|
+
class ChatEngine:
|
|
16
|
+
"""Conversational chat engine for Sky."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config, models_config, db, router):
|
|
19
|
+
self.config = config
|
|
20
|
+
self.models_config = models_config
|
|
21
|
+
self.db = db
|
|
22
|
+
self.router = router
|
|
23
|
+
self.messages = []
|
|
24
|
+
self.session_id = db.create_session("chat", "conversation")
|
|
25
|
+
self.console = Console()
|
|
26
|
+
|
|
27
|
+
def get_welcome_message(self) -> str:
|
|
28
|
+
"""Return welcome message with capabilities."""
|
|
29
|
+
return """
|
|
30
|
+
☁️ **Sky Chat Mode**
|
|
31
|
+
|
|
32
|
+
I'm Sky, your AI coding assistant. I can help you:
|
|
33
|
+
|
|
34
|
+
• **Answer questions** about your codebase → `/ask`
|
|
35
|
+
• **Plan features** with structured tasks → `/plan`
|
|
36
|
+
• **Write and edit code** with approval → `/agent`
|
|
37
|
+
• **Run complex workflows** with subagents → `/workflow`
|
|
38
|
+
|
|
39
|
+
**Try asking me:**
|
|
40
|
+
- "What can you do?"
|
|
41
|
+
- "Explain the approval gate"
|
|
42
|
+
- "Help me fix a bug"
|
|
43
|
+
- "Plan a new feature"
|
|
44
|
+
|
|
45
|
+
Just type your message, and I'll guide you to the right tool!
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def suggest_command(self, user_input: str) -> Optional[str]:
|
|
49
|
+
"""Suggest the right Sky command for the user's request."""
|
|
50
|
+
# Simple keyword-based routing
|
|
51
|
+
user_input_lower = user_input.lower()
|
|
52
|
+
|
|
53
|
+
if any(word in user_input_lower for word in ["explain", "what", "how", "why", "tell me"]):
|
|
54
|
+
return f"sky ask \"{user_input}\""
|
|
55
|
+
|
|
56
|
+
if any(word in user_input_lower for word in ["fix", "bug", "error", "issue", "problem"]):
|
|
57
|
+
return f"sky agent \"{user_input}\""
|
|
58
|
+
|
|
59
|
+
if any(word in user_input_lower for word in ["plan", "design", "architecture", "feature"]):
|
|
60
|
+
return f"sky plan \"{user_input}\""
|
|
61
|
+
|
|
62
|
+
if any(word in user_input_lower for word in ["build", "add", "implement", "create", "workflow"]):
|
|
63
|
+
return f"sky workflow \"{user_input}\""
|
|
64
|
+
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def run(self, initial_prompt: Optional[str] = None):
|
|
68
|
+
"""Run the chat loop."""
|
|
69
|
+
self.console.print(Panel(
|
|
70
|
+
Markdown(self.get_welcome_message()),
|
|
71
|
+
title="☁️ Sky Chat",
|
|
72
|
+
border_style="cyan"
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
if initial_prompt:
|
|
76
|
+
self._process_user_message(initial_prompt)
|
|
77
|
+
|
|
78
|
+
while True:
|
|
79
|
+
user_input = Prompt.ask("\n[bold cyan]You[/bold cyan]")
|
|
80
|
+
|
|
81
|
+
if user_input.lower() in ["exit", "quit", "bye"]:
|
|
82
|
+
self.console.print("[dim]Goodbye! 👋[/dim]")
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
if user_input.startswith("/"):
|
|
86
|
+
self._handle_command(user_input)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
self._process_user_message(user_input)
|
|
90
|
+
|
|
91
|
+
def _process_user_message(self, user_input: str):
|
|
92
|
+
"""Process user message and generate response."""
|
|
93
|
+
|
|
94
|
+
# Check for identity questions
|
|
95
|
+
if any(phrase in user_input.lower() for phrase in ["who built", "who created", "who developed", "who made you"]):
|
|
96
|
+
self._respond_with_creator_info()
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Check if we should suggest a command
|
|
100
|
+
suggested = self.suggest_command(user_input)
|
|
101
|
+
|
|
102
|
+
if suggested:
|
|
103
|
+
self.console.print(f"\n[dim]💡 I can help you with that! Try:[/dim]")
|
|
104
|
+
self.console.print(f" [bold cyan]{suggested}[/bold cyan]")
|
|
105
|
+
|
|
106
|
+
if Confirm.ask("\n[dim]Would you like me to run this for you?[/dim]", default=False):
|
|
107
|
+
self.console.print(f"\n[dim]⏳ Running: {suggested}[/dim]")
|
|
108
|
+
# Execute the command
|
|
109
|
+
import subprocess
|
|
110
|
+
import shlex
|
|
111
|
+
try:
|
|
112
|
+
parts = shlex.split(suggested)
|
|
113
|
+
except ValueError:
|
|
114
|
+
parts = suggested.split()
|
|
115
|
+
|
|
116
|
+
if sys.platform == "win32":
|
|
117
|
+
cmd = ["python", "-m", "sky.cli"] + parts[1:]
|
|
118
|
+
else:
|
|
119
|
+
cmd = parts
|
|
120
|
+
subprocess.run(cmd, check=False)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Fallback: use chat model for conversation
|
|
124
|
+
import asyncio
|
|
125
|
+
asyncio.run(self._get_chat_response(user_input))
|
|
126
|
+
|
|
127
|
+
def _respond_with_creator_info(self):
|
|
128
|
+
"""Respond with creator information."""
|
|
129
|
+
response = """
|
|
130
|
+
I was built by **Aaditya A** 👨💻
|
|
131
|
+
|
|
132
|
+
- **Role:** AI/ML Intern at CoRover.ai
|
|
133
|
+
- **About:** MCA - AI/ML Final year student at JAIN UNIVERSITY, BANGALORE
|
|
134
|
+
|
|
135
|
+
Sky is a local, CLI-based, agentic software development assistant that helps developers plan, write, test, and self-correct code.
|
|
136
|
+
"""
|
|
137
|
+
self.console.print(f"\n[bold cyan]Sky[/bold cyan]")
|
|
138
|
+
self.console.print(Markdown(response))
|
|
139
|
+
self.messages.append({"role": "user", "content": "Who built you?"})
|
|
140
|
+
self.messages.append({"role": "assistant", "content": response})
|
|
141
|
+
|
|
142
|
+
async def _get_chat_response(self, user_input: str):
|
|
143
|
+
"""Get response from chat model."""
|
|
144
|
+
if not any(m.get("role") == "system" for m in self.messages):
|
|
145
|
+
from sky.core.mode_prompts import get_mode_prompt
|
|
146
|
+
self.messages.insert(0, {"role": "system", "content": get_mode_prompt("chat")})
|
|
147
|
+
|
|
148
|
+
self.messages.append({"role": "user", "content": user_input})
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
with self.console.status("[bold cyan]Thinking...", spinner="dots"):
|
|
152
|
+
response, was_fallback, model_used = await self.router.route(
|
|
153
|
+
"general",
|
|
154
|
+
self.messages,
|
|
155
|
+
expected_tool_calls=0
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
content = response.get("content", "")
|
|
159
|
+
|
|
160
|
+
self.console.print(f"\n[bold cyan]Sky[/bold cyan]")
|
|
161
|
+
self.console.print(Markdown(content))
|
|
162
|
+
|
|
163
|
+
self.messages.append({"role": "assistant", "content": content})
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
self.console.print(f"[red]Error: {e}[/red]")
|
|
167
|
+
|
|
168
|
+
def _handle_command(self, user_input: str):
|
|
169
|
+
"""Handle slash commands."""
|
|
170
|
+
import sys
|
|
171
|
+
import shlex
|
|
172
|
+
try:
|
|
173
|
+
parts = shlex.split(user_input)
|
|
174
|
+
except ValueError:
|
|
175
|
+
parts = user_input.split()
|
|
176
|
+
|
|
177
|
+
cmd = parts[0].lower()
|
|
178
|
+
args = parts[1:]
|
|
179
|
+
|
|
180
|
+
if cmd == "/ask":
|
|
181
|
+
self.console.print("[dim]Switching to /ask mode...[/dim]")
|
|
182
|
+
import subprocess
|
|
183
|
+
if sys.platform == "win32":
|
|
184
|
+
subprocess.run(["python", "-m", "sky.cli", "ask"] + args, check=False)
|
|
185
|
+
else:
|
|
186
|
+
subprocess.run(["sky", "ask"] + args, check=False)
|
|
187
|
+
elif cmd == "/agent":
|
|
188
|
+
self.console.print("[dim]Switching to /agent mode...[/dim]")
|
|
189
|
+
import subprocess
|
|
190
|
+
if sys.platform == "win32":
|
|
191
|
+
subprocess.run(["python", "-m", "sky.cli", "agent"] + args, check=False)
|
|
192
|
+
else:
|
|
193
|
+
subprocess.run(["sky", "agent"] + args, check=False)
|
|
194
|
+
elif cmd == "/workflow":
|
|
195
|
+
self.console.print("[dim]Switching to /workflow mode...[/dim]")
|
|
196
|
+
import subprocess
|
|
197
|
+
if sys.platform == "win32":
|
|
198
|
+
subprocess.run(["python", "-m", "sky.cli", "workflow"] + args, check=False)
|
|
199
|
+
else:
|
|
200
|
+
subprocess.run(["sky", "workflow"] + args, check=False)
|
|
201
|
+
elif cmd == "/plan":
|
|
202
|
+
self.console.print("[dim]Switching to /plan mode...[/dim]")
|
|
203
|
+
import subprocess
|
|
204
|
+
if sys.platform == "win32":
|
|
205
|
+
subprocess.run(["python", "-m", "sky.cli", "plan"] + args, check=False)
|
|
206
|
+
else:
|
|
207
|
+
subprocess.run(["sky", "plan"] + args, check=False)
|
|
208
|
+
else:
|
|
209
|
+
self.console.print(f"[red]Unknown command: {cmd}[/red]")
|
|
210
|
+
|
|
211
|
+
import sys
|
sky/core/fast_loop.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Fast Loop Engine for iterative agent execution."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, AsyncIterator, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from sky.config import DexProjectConfig
|
|
8
|
+
from sky.core.approval import ApprovalGate
|
|
9
|
+
from sky.core.mode_prompts import get_mode_prompt
|
|
10
|
+
from sky.core.router import ModelRouter
|
|
11
|
+
from sky.storage import DatabaseManager
|
|
12
|
+
from sky.tools.registry import RiskTier, get_tool, get_tool_schemas
|
|
13
|
+
from sky.security import get_security_guardrails
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FastLoopEngine:
|
|
17
|
+
"""Core engine for running the agent loop."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: DexProjectConfig, db: DatabaseManager, router: ModelRouter, session_id: str, approval_gate: Optional[ApprovalGate] = None, indexer: Optional[Any] = None):
|
|
20
|
+
self.config = config
|
|
21
|
+
self.db = db
|
|
22
|
+
self.router = router
|
|
23
|
+
self.approval_gate = approval_gate
|
|
24
|
+
self.session_id = session_id
|
|
25
|
+
self.indexer = indexer
|
|
26
|
+
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
if self.approval_gate is None:
|
|
29
|
+
Console().print("[bold yellow]WARNING: FastLoopEngine initialized without approval_gate. Destructive tools will be blocked.[/bold yellow]")
|
|
30
|
+
else:
|
|
31
|
+
if self.config.verbose:
|
|
32
|
+
Console().print("[dim]approval_gate passed to FastLoopEngine[/dim]")
|
|
33
|
+
|
|
34
|
+
def _get_available_tools(self, mode: str, tools_filter: Optional[List[str]] = None) -> List[Dict[str, Any]]:
|
|
35
|
+
"""Filter tools based on mode and specific filter list."""
|
|
36
|
+
all_schemas = get_tool_schemas()
|
|
37
|
+
|
|
38
|
+
if mode == "ask":
|
|
39
|
+
safe_schemas = []
|
|
40
|
+
for schema in all_schemas:
|
|
41
|
+
tool_def = get_tool(schema["function"]["name"])
|
|
42
|
+
if tool_def and tool_def.risk_tier == RiskTier.SAFE:
|
|
43
|
+
safe_schemas.append(schema)
|
|
44
|
+
all_schemas = safe_schemas
|
|
45
|
+
elif mode == "agent":
|
|
46
|
+
# Explicitly keep ALL tools for agent mode
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
if tools_filter:
|
|
50
|
+
all_schemas = [s for s in all_schemas if s["function"]["name"] in tools_filter]
|
|
51
|
+
|
|
52
|
+
return all_schemas
|
|
53
|
+
|
|
54
|
+
async def _execute_safe_tool(self, tool_call_id: str, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
55
|
+
"""Execute a single safe tool asynchronously."""
|
|
56
|
+
tool_def = get_tool(tool_name)
|
|
57
|
+
if not tool_def:
|
|
58
|
+
result_str = f"Error: Tool {tool_name} not found."
|
|
59
|
+
else:
|
|
60
|
+
try:
|
|
61
|
+
result = tool_def.func(**args)
|
|
62
|
+
result_str = json.dumps(result) if not isinstance(result, str) else result
|
|
63
|
+
except Exception as e:
|
|
64
|
+
result_str = f"Error: {e}"
|
|
65
|
+
|
|
66
|
+
self.db.log_tool_call(self.session_id, tool_name, args, "safe", "auto_approved", result=result_str)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"tool_call_id": tool_call_id,
|
|
70
|
+
"role": "tool",
|
|
71
|
+
"name": tool_name,
|
|
72
|
+
"content": result_str,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async def _execute_destructive_tool(self, tool_call_id: str, tool_name: str, args: Dict[str, Any], mode: str) -> Dict[str, Any]:
|
|
76
|
+
"""Execute a destructive tool sequentially with approval gate."""
|
|
77
|
+
from rich.console import Console
|
|
78
|
+
console = Console()
|
|
79
|
+
if self.config.verbose:
|
|
80
|
+
console.print(f"[dim]_execute_destructive_tool called for {tool_name}[/dim]")
|
|
81
|
+
|
|
82
|
+
tool_def = get_tool(tool_name)
|
|
83
|
+
if not tool_def:
|
|
84
|
+
result_str = f"Error: Tool {tool_name} not found."
|
|
85
|
+
self.db.log_tool_call(self.session_id, tool_name, args, "destructive", "rejected", result=result_str)
|
|
86
|
+
return {
|
|
87
|
+
"tool_call_id": tool_call_id,
|
|
88
|
+
"role": "tool",
|
|
89
|
+
"name": tool_name,
|
|
90
|
+
"content": result_str,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if self.approval_gate is None:
|
|
94
|
+
console.print(f"[bold red]CRITICAL: approval_gate is None! Rejecting {tool_name}[/bold red]")
|
|
95
|
+
return {
|
|
96
|
+
"tool_call_id": tool_call_id,
|
|
97
|
+
"role": "tool",
|
|
98
|
+
"name": tool_name,
|
|
99
|
+
"content": f"Execution rejected: approval_gate is not initialized.",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
approved, reason, final_args = self.approval_gate.process(tool_name, args)
|
|
103
|
+
|
|
104
|
+
if self.config.verbose:
|
|
105
|
+
console.print(f"[dim]approval_gate.process() returned decision: approved={approved}, reason={reason}[/dim]")
|
|
106
|
+
|
|
107
|
+
if not approved:
|
|
108
|
+
result_str = f"Execution rejected by user: {reason}"
|
|
109
|
+
else:
|
|
110
|
+
execute_args = final_args or args
|
|
111
|
+
try:
|
|
112
|
+
result = tool_def.func(**execute_args)
|
|
113
|
+
result_str = json.dumps(result) if not isinstance(result, str) else result
|
|
114
|
+
except Exception as e:
|
|
115
|
+
result_str = f"Error: {e}"
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"tool_call_id": tool_call_id,
|
|
119
|
+
"role": "tool",
|
|
120
|
+
"name": tool_name,
|
|
121
|
+
"content": result_str,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def _validate_tool_calls(self, tool_calls: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
|
125
|
+
"""Validate that tool calls are well-formed JSON. Returns (valid_calls, error_results)."""
|
|
126
|
+
valid_calls = []
|
|
127
|
+
error_results = []
|
|
128
|
+
for tc in tool_calls:
|
|
129
|
+
try:
|
|
130
|
+
if not isinstance(tc, dict):
|
|
131
|
+
raise ValueError("Tool call is not a dictionary")
|
|
132
|
+
if "function" not in tc or "name" not in tc["function"]:
|
|
133
|
+
raise ValueError("Missing 'function' or 'name' in tool call")
|
|
134
|
+
|
|
135
|
+
tool_name = tc["function"]["name"]
|
|
136
|
+
args_raw = tc["function"].get("arguments", "{}")
|
|
137
|
+
|
|
138
|
+
if isinstance(args_raw, str):
|
|
139
|
+
try:
|
|
140
|
+
args_dict = json.loads(args_raw)
|
|
141
|
+
except json.JSONDecodeError as e:
|
|
142
|
+
raise ValueError(f"Arguments are not valid JSON: {e}")
|
|
143
|
+
else:
|
|
144
|
+
args_dict = args_raw
|
|
145
|
+
|
|
146
|
+
if getattr(self.config, "security_enabled", True):
|
|
147
|
+
guardrails = get_security_guardrails()
|
|
148
|
+
guardrails.strict_mode = getattr(self.config, "security_strict_mode", True)
|
|
149
|
+
is_valid, err_msg = guardrails.validate_tool_call(tool_name, args_dict)
|
|
150
|
+
if not is_valid:
|
|
151
|
+
error_results.append({
|
|
152
|
+
"tool_call_id": tc.get("id", "unknown"),
|
|
153
|
+
"role": "tool",
|
|
154
|
+
"name": tool_name,
|
|
155
|
+
"content": f"Security Guardrail Blocked: {err_msg}"
|
|
156
|
+
})
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
valid_calls.append(tc)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
from rich.console import Console
|
|
162
|
+
Console().print(f"[bold red]Skipping invalid tool call: {e}[/bold red]")
|
|
163
|
+
error_results.append({
|
|
164
|
+
"tool_call_id": tc.get("id", "unknown") if isinstance(tc, dict) else "unknown",
|
|
165
|
+
"role": "tool",
|
|
166
|
+
"name": tc.get("function", {}).get("name", "unknown") if isinstance(tc, dict) and "function" in tc else "unknown",
|
|
167
|
+
"content": f"System Error: Invalid tool call format - {e}. You MUST use native JSON tool calls. DO NOT output XML."
|
|
168
|
+
})
|
|
169
|
+
return valid_calls, error_results
|
|
170
|
+
|
|
171
|
+
async def _execute_tool_calls(self, tool_calls: List[Dict[str, Any]], mode: str) -> List[Dict[str, Any]]:
|
|
172
|
+
"""Execute a list of tool calls, handling risk tiers appropriately."""
|
|
173
|
+
results = []
|
|
174
|
+
safe_tasks = []
|
|
175
|
+
|
|
176
|
+
valid_calls, error_results = self._validate_tool_calls(tool_calls)
|
|
177
|
+
results.extend(error_results)
|
|
178
|
+
|
|
179
|
+
for tc in valid_calls:
|
|
180
|
+
tool_name = tc["function"]["name"]
|
|
181
|
+
try:
|
|
182
|
+
args = json.loads(tc["function"]["arguments"])
|
|
183
|
+
except Exception:
|
|
184
|
+
args = {}
|
|
185
|
+
|
|
186
|
+
tool_def = get_tool(tool_name)
|
|
187
|
+
|
|
188
|
+
if tool_def and tool_def.risk_tier == RiskTier.SAFE:
|
|
189
|
+
safe_tasks.append(self._execute_safe_tool(tc["id"], tool_name, args))
|
|
190
|
+
else:
|
|
191
|
+
if safe_tasks:
|
|
192
|
+
safe_results = await asyncio.gather(*safe_tasks)
|
|
193
|
+
results.extend(safe_results)
|
|
194
|
+
safe_tasks = []
|
|
195
|
+
|
|
196
|
+
res = await self._execute_destructive_tool(tc["id"], tool_name, args, mode)
|
|
197
|
+
results.append(res)
|
|
198
|
+
|
|
199
|
+
if safe_tasks:
|
|
200
|
+
safe_results = await asyncio.gather(*safe_tasks)
|
|
201
|
+
results.extend(safe_results)
|
|
202
|
+
|
|
203
|
+
return results
|
|
204
|
+
|
|
205
|
+
def _get_relevant_context(self, query: str) -> str:
|
|
206
|
+
"""Retrieve relevant context for the query if memory is enabled."""
|
|
207
|
+
if not self.indexer or not getattr(self.config, "memory_enabled", False):
|
|
208
|
+
return ""
|
|
209
|
+
|
|
210
|
+
from rich.console import Console
|
|
211
|
+
if self.config.verbose:
|
|
212
|
+
Console().print(f"[dim]Retrieving context for query...[/dim]")
|
|
213
|
+
|
|
214
|
+
context = self.indexer.get_context(query, top_k=getattr(self.config, "memory_top_k", 5))
|
|
215
|
+
if not context:
|
|
216
|
+
return ""
|
|
217
|
+
|
|
218
|
+
return context
|
|
219
|
+
|
|
220
|
+
async def run(
|
|
221
|
+
self,
|
|
222
|
+
messages: List[Dict[str, Any]],
|
|
223
|
+
mode: str,
|
|
224
|
+
system_prompt: Optional[str] = None,
|
|
225
|
+
max_turns: Optional[int] = None,
|
|
226
|
+
tools_filter: Optional[List[str]] = None,
|
|
227
|
+
inject_context: bool = False
|
|
228
|
+
) -> AsyncIterator[Dict[str, Any]]:
|
|
229
|
+
"""Run the main agent loop streaming events."""
|
|
230
|
+
turn_limit = max_turns or self.config.max_turn_limit
|
|
231
|
+
if getattr(self.config, "security_enabled", True):
|
|
232
|
+
from sky.security.prompts import get_system_prompt_with_guardrails, get_hardened_system_prompt
|
|
233
|
+
if system_prompt:
|
|
234
|
+
prompt = get_hardened_system_prompt(system_prompt)
|
|
235
|
+
else:
|
|
236
|
+
prompt = get_system_prompt_with_guardrails(mode)
|
|
237
|
+
else:
|
|
238
|
+
prompt = system_prompt or get_mode_prompt(mode)
|
|
239
|
+
|
|
240
|
+
# Get context from first user message
|
|
241
|
+
user_query = ""
|
|
242
|
+
for msg in messages:
|
|
243
|
+
if msg.get("role") == "user":
|
|
244
|
+
user_query = str(msg.get("content", ""))
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
from sky.core.mode_prompts import format_context_prompt
|
|
248
|
+
if inject_context and self.indexer and getattr(self.config, "memory_enabled", False):
|
|
249
|
+
context = self._get_relevant_context(user_query)
|
|
250
|
+
prompt = format_context_prompt(prompt, context)
|
|
251
|
+
else:
|
|
252
|
+
prompt = format_context_prompt(prompt, "")
|
|
253
|
+
|
|
254
|
+
if getattr(self.config, "security_enabled", True):
|
|
255
|
+
from sky.security.sanitize import validate_context_size
|
|
256
|
+
if not validate_context_size(messages, max_tokens=getattr(self.config, "security_max_input_length", 10000)):
|
|
257
|
+
yield {"type": "error", "message": "Context size exceeds limit. Please start a new session."}
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
if not messages or messages[0].get("role") != "system":
|
|
261
|
+
messages.insert(0, {"role": "system", "content": prompt})
|
|
262
|
+
else:
|
|
263
|
+
messages[0]["content"] = prompt
|
|
264
|
+
|
|
265
|
+
tools = self._get_available_tools(mode, tools_filter)
|
|
266
|
+
role = "fast_loop" if mode == "agent" else "routing"
|
|
267
|
+
|
|
268
|
+
if self.config.verbose:
|
|
269
|
+
from rich.console import Console
|
|
270
|
+
Console().print(f"[dim]Available tools for {mode}: {[t['function']['name'] for t in tools]}[/dim]")
|
|
271
|
+
|
|
272
|
+
for turn in range(turn_limit):
|
|
273
|
+
yield {"type": "turn_start", "turn": turn + 1}
|
|
274
|
+
|
|
275
|
+
expected = 1
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
response_msg, was_fallback, model_used = await self.router.route(
|
|
279
|
+
role=role,
|
|
280
|
+
messages=messages,
|
|
281
|
+
tools=tools if tools else None,
|
|
282
|
+
expected_tool_calls=expected if tools else None
|
|
283
|
+
)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
error_str = str(e)
|
|
286
|
+
if "tool call validation failed" in error_str or "invalid_request_error" in error_str or "400" in error_str:
|
|
287
|
+
from rich.console import Console
|
|
288
|
+
Console().print(f"[yellow]Model tool validation failed. Feeding error back to model...[/yellow]")
|
|
289
|
+
messages.append({
|
|
290
|
+
"role": "user",
|
|
291
|
+
"content": f"System Error: Your last response triggered an API validation error: {error_str}\n\nYou MUST use native JSON tool calls. DO NOT output XML pseudo-tags like <tool_call>. Only call tools that are explicitly provided in the schema."
|
|
292
|
+
})
|
|
293
|
+
continue
|
|
294
|
+
else:
|
|
295
|
+
raise e
|
|
296
|
+
|
|
297
|
+
messages.append(response_msg)
|
|
298
|
+
|
|
299
|
+
yield {
|
|
300
|
+
"type": "model_response",
|
|
301
|
+
"message": response_msg,
|
|
302
|
+
"was_fallback": was_fallback,
|
|
303
|
+
"model": model_used
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if "tool_calls" in response_msg and response_msg["tool_calls"]:
|
|
307
|
+
for tc in response_msg["tool_calls"]:
|
|
308
|
+
if "function" in tc:
|
|
309
|
+
yield {
|
|
310
|
+
"type": "tool_call",
|
|
311
|
+
"tool_name": tc["function"].get("name", "unknown"),
|
|
312
|
+
"args": tc["function"].get("arguments", "{}")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
tool_results = await self._execute_tool_calls(response_msg["tool_calls"], mode)
|
|
316
|
+
messages.extend(tool_results)
|
|
317
|
+
|
|
318
|
+
for res in tool_results:
|
|
319
|
+
yield {
|
|
320
|
+
"type": "tool_result",
|
|
321
|
+
"tool_name": res.get("name", "unknown"),
|
|
322
|
+
"result": res.get("content", "")
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
yield {"type": "tool_results", "results": tool_results}
|
|
326
|
+
else:
|
|
327
|
+
yield {"type": "final_answer", "content": response_msg.get("content", "")}
|
|
328
|
+
break
|
|
329
|
+
else:
|
|
330
|
+
yield {"type": "error", "content": f"Turn limit of {turn_limit} exceeded."}
|
sky/core/mode_prompts.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""System prompts for different SKY operational modes."""
|
|
2
|
+
|
|
3
|
+
ASK_SYSTEM_PROMPT = """You are SKY, a local AI coding assistant running directly on the user's machine.
|
|
4
|
+
Your role is to answer questions, explain code, and read files.
|
|
5
|
+
You are in READ-ONLY mode. You may use SAFE tools to gather information.
|
|
6
|
+
Do NOT attempt to modify any files or run destructive commands.
|
|
7
|
+
Do NOT introduce yourself as ChatGPT or an AI from OpenAI. You are SKY.
|
|
8
|
+
Provide clear, concise, and helpful answers.
|
|
9
|
+
|
|
10
|
+
{CONTEXT_PLACEHOLDER}"""
|
|
11
|
+
|
|
12
|
+
AGENT_SYSTEM_PROMPT = """You are SKY, an autonomous AI software engineer running locally on the user's machine.
|
|
13
|
+
Your role is to execute full software development tasks by using the provided tools.
|
|
14
|
+
Do NOT introduce yourself as ChatGPT or an AI from OpenAI. You are SKY.
|
|
15
|
+
You MUST use the native JSON tool calling mechanism provided by the system.
|
|
16
|
+
DO NOT output pseudo-tags like <tool_call>, <function=...>, <parameter=...>.
|
|
17
|
+
DO NOT write XML or HTML for tool calls. DO NOT write code for the user to run manually; use your tools (like bash or edit_file) to execute it directly.
|
|
18
|
+
|
|
19
|
+
Example of correct internal native JSON tool call format:
|
|
20
|
+
{
|
|
21
|
+
"name": "edit_file",
|
|
22
|
+
"arguments": {
|
|
23
|
+
"path": "file.py",
|
|
24
|
+
"diff": "..."
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
You have access to all tools, including DESTRUCTIVE ones (edit files, run tests, run shell commands).
|
|
29
|
+
Work methodically:
|
|
30
|
+
1. Gather information (read files, run grep, git status)
|
|
31
|
+
2. Make changes (edit files)
|
|
32
|
+
3. Verify your changes (run tests, lint)
|
|
33
|
+
4. Self-correct if errors occur.
|
|
34
|
+
|
|
35
|
+
If you need to make multiple tool calls (e.g. read multiple files), execute them in parallel when possible.
|
|
36
|
+
Provide a final textual answer only when the task is complete.
|
|
37
|
+
|
|
38
|
+
{CONTEXT_PLACEHOLDER}"""
|
|
39
|
+
|
|
40
|
+
PLAN_SYSTEM_PROMPT = """You are SKY in PLANNING mode.
|
|
41
|
+
Your role is to analyze a request and produce a structured execution plan.
|
|
42
|
+
Do NOT execute the plan.
|
|
43
|
+
First, research the codebase using SAFE tools.
|
|
44
|
+
Once you have enough context, output a structured JSON plan."""
|
|
45
|
+
|
|
46
|
+
CHAT_SYSTEM_PROMPT = """You are Sky, an agentic coding assistant.
|
|
47
|
+
|
|
48
|
+
Identity:
|
|
49
|
+
- Purpose: Help developers plan, write, test, and understand code
|
|
50
|
+
|
|
51
|
+
Capabilities:
|
|
52
|
+
- Answer questions about codebases
|
|
53
|
+
- Plan features with structured tasks
|
|
54
|
+
- Write and edit code (with human approval)
|
|
55
|
+
- Run complex workflows with subagents
|
|
56
|
+
- Search code semantically
|
|
57
|
+
|
|
58
|
+
Personality:
|
|
59
|
+
- Friendly, helpful, and concise
|
|
60
|
+
- Focus on coding assistance
|
|
61
|
+
- Guide users to Sky's features
|
|
62
|
+
- Be transparent about capabilities and limitations
|
|
63
|
+
|
|
64
|
+
When users ask about general topics, politely redirect to coding assistance.
|
|
65
|
+
|
|
66
|
+
You are NOT ChatGPT, Claude, or any other AI. You are Sky.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
PLAN_WRITE_PROMPT = """{
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"goal": {"type": "string"},
|
|
73
|
+
"steps": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"items": {"type": "string"}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"required": ["goal", "steps"]
|
|
79
|
+
}"""
|
|
80
|
+
|
|
81
|
+
def get_mode_prompt(mode: str) -> str:
|
|
82
|
+
"""Return the appropriate system prompt for the given mode."""
|
|
83
|
+
mode = mode.lower()
|
|
84
|
+
if mode == "ask":
|
|
85
|
+
return ASK_SYSTEM_PROMPT
|
|
86
|
+
elif mode == "agent":
|
|
87
|
+
return AGENT_SYSTEM_PROMPT
|
|
88
|
+
elif mode == "plan":
|
|
89
|
+
return PLAN_SYSTEM_PROMPT
|
|
90
|
+
elif mode == "chat":
|
|
91
|
+
return CHAT_SYSTEM_PROMPT
|
|
92
|
+
else:
|
|
93
|
+
return AGENT_SYSTEM_PROMPT
|
|
94
|
+
|
|
95
|
+
def format_context_prompt(prompt: str, context: str) -> str:
|
|
96
|
+
"""Format the system prompt by replacing the context placeholder.
|
|
97
|
+
|
|
98
|
+
If context is provided, it replaces the placeholder with the repository context section.
|
|
99
|
+
Otherwise, it strips out the placeholder.
|
|
100
|
+
"""
|
|
101
|
+
if context:
|
|
102
|
+
context_section = f"## Repository Context\n\nThe system has retrieved relevant code from your repository to help answer your question.\nThis context is injected below. Use it to inform your responses.\n\n{context}"
|
|
103
|
+
return prompt.replace("{CONTEXT_PLACEHOLDER}", context_section)
|
|
104
|
+
else:
|
|
105
|
+
return prompt.replace("{CONTEXT_PLACEHOLDER}", "").strip()
|
|
106
|
+
|