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/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."}
@@ -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
+