gateway/modules/features/codeeditor/promptAssembly.py
2026-02-23 22:09:27 +01:00

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)