183 lines
5.9 KiB
Python
183 lines
5.9 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""Prompt assembly for the CodeEditor feature.
|
|
Builds Cursor-style system prompts with file context and format instructions."""
|
|
|
|
import logging
|
|
from typing import List, Optional, Dict, Any
|
|
|
|
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
|
|
from modules.features.codeeditor.datamodelCodeeditor import FileContext
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SYSTEM_PROMPT = """You are an AI assistant for text and code file editing. You receive files as context and can suggest changes.
|
|
|
|
## Rules for file edits
|
|
- Use ```file_edit``` blocks for file changes
|
|
- Each file_edit block must contain: fileName, oldContent (exact text to replace), newContent (replacement text)
|
|
- Explain changes in normal text before or after the block
|
|
- oldContent must EXACTLY match existing content (including whitespace and indentation)
|
|
- You may propose edits to multiple files in one response
|
|
|
|
## Response format
|
|
Normal text is displayed as explanation.
|
|
File changes must use this format:
|
|
|
|
```file_edit
|
|
fileName: <filename>
|
|
oldContent: |
|
|
<exact existing content to replace>
|
|
newContent: |
|
|
<new replacement content>
|
|
```
|
|
|
|
Code examples (without edits) use standard markdown code blocks:
|
|
```language
|
|
code here
|
|
```
|
|
|
|
## Important
|
|
- Only edit files that are provided in context
|
|
- Make minimal, targeted changes
|
|
- Preserve existing formatting and style
|
|
- If a task is unclear, ask for clarification instead of guessing"""
|
|
|
|
|
|
def buildRequest(
|
|
userPrompt: str,
|
|
fileContexts: List[FileContext],
|
|
chatHistory: Optional[List[Dict[str, Any]]] = None
|
|
) -> AiCallRequest:
|
|
"""Build an AiCallRequest with system prompt, file context, and user prompt."""
|
|
systemPart = SYSTEM_PROMPT
|
|
fileContextPart = _buildFileContext(fileContexts)
|
|
historyPart = _buildChatHistory(chatHistory) if chatHistory else ""
|
|
|
|
fullPrompt = systemPart
|
|
if historyPart:
|
|
fullPrompt += f"\n\n## Previous conversation\n{historyPart}"
|
|
fullPrompt += f"\n\n## User request\n{userPrompt}"
|
|
|
|
return AiCallRequest(
|
|
prompt=fullPrompt,
|
|
context=fileContextPart if fileContextPart else None,
|
|
options=AiCallOptions(
|
|
operationType=OperationTypeEnum.DATA_ANALYSE,
|
|
temperature=0.0,
|
|
compressPrompt=False,
|
|
compressContext=False,
|
|
resultFormat="txt"
|
|
)
|
|
)
|
|
|
|
|
|
def _buildFileContext(fileContexts: List[FileContext]) -> str:
|
|
"""Build the file context string with line numbers."""
|
|
if not fileContexts:
|
|
return ""
|
|
|
|
parts = []
|
|
for fc in fileContexts:
|
|
if not fc.content:
|
|
continue
|
|
lines = fc.content.split("\n")
|
|
numberedLines = [f"{i + 1}|{line}" for i, line in enumerate(lines)]
|
|
numbered = "\n".join(numberedLines)
|
|
parts.append(f"--- FILE: {fc.fileName} ---\n{numbered}\n--- END FILE ---")
|
|
|
|
return "\n\n".join(parts)
|
|
|
|
|
|
def buildAgentRequest(
|
|
userPrompt: Optional[str],
|
|
fileListContext: str,
|
|
conversationHistory: List[Dict[str, Any]]
|
|
) -> AiCallRequest:
|
|
"""Build an AiCallRequest for agent mode with tool definitions and conversation history."""
|
|
from modules.features.codeeditor.toolRegistry import formatToolDefinitions
|
|
|
|
systemPrompt = _AGENT_SYSTEM_PROMPT.replace("{{TOOL_DEFINITIONS}}", formatToolDefinitions())
|
|
|
|
if not conversationHistory:
|
|
fullPrompt = systemPrompt
|
|
context = f"## Available files\n{fileListContext}\n\n## Task\n{userPrompt}"
|
|
else:
|
|
fullPrompt = systemPrompt
|
|
historyText = _buildConversationHistory(conversationHistory)
|
|
context = f"## Available files\n{fileListContext}\n\n## Conversation\n{historyText}"
|
|
|
|
return AiCallRequest(
|
|
prompt=fullPrompt,
|
|
context=context,
|
|
options=AiCallOptions(
|
|
operationType=OperationTypeEnum.DATA_ANALYSE,
|
|
temperature=0.0,
|
|
compressPrompt=False,
|
|
compressContext=False,
|
|
resultFormat="txt"
|
|
)
|
|
)
|
|
|
|
|
|
_AGENT_SYSTEM_PROMPT = """You are an AI agent for file analysis and editing. You work autonomously by using tools to read files, search content, and propose edits.
|
|
|
|
## Available tools
|
|
{{TOOL_DEFINITIONS}}
|
|
|
|
## How to call tools
|
|
Use this exact format for each tool call:
|
|
|
|
```tool_call
|
|
tool: <tool_name>
|
|
args: {"param": "value"}
|
|
```
|
|
|
|
## Rules
|
|
- Read files ONE AT A TIME with read_file, never assume file contents
|
|
- First create a plan, then execute it step by step
|
|
- Use search_files to find relevant files before reading them
|
|
- Use list_files to discover what files are available
|
|
- For file changes, use ```file_edit``` blocks (same format as before)
|
|
- You may combine text explanations, tool calls, and file edits in one response
|
|
- When you are DONE and need no more tool calls, simply respond with text only (no tool_call blocks)
|
|
- Keep responses focused and efficient
|
|
|
|
## file_edit format (for changes)
|
|
```file_edit
|
|
fileName: <filename>
|
|
oldContent: |
|
|
<exact existing content>
|
|
newContent: |
|
|
<replacement content>
|
|
```"""
|
|
|
|
|
|
def _buildConversationHistory(history: List[Dict[str, Any]]) -> str:
|
|
"""Build the full conversation history for agent multi-turn context."""
|
|
parts = []
|
|
for msg in history:
|
|
role = msg.get("role", "unknown")
|
|
content = msg.get("content", "")
|
|
if role == "tool_result":
|
|
toolName = msg.get("toolName", "")
|
|
parts.append(f"[Tool Result - {toolName}]:\n{content}")
|
|
else:
|
|
parts.append(f"[{role}]:\n{content}")
|
|
return "\n\n".join(parts)
|
|
|
|
|
|
def _buildChatHistory(chatHistory: List[Dict[str, Any]]) -> str:
|
|
"""Build a condensed chat history string for multi-turn context."""
|
|
if not chatHistory:
|
|
return ""
|
|
|
|
parts = []
|
|
for msg in chatHistory[-10:]:
|
|
role = msg.get("role", "unknown")
|
|
content = msg.get("content", "")
|
|
if len(content) > 500:
|
|
content = content[:500] + "..."
|
|
parts.append(f"[{role}]: {content}")
|
|
|
|
return "\n".join(parts)
|