feat: implement Agentic Skill and Negative Prompt injection
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "g1nation",
|
"name": "g1nation",
|
||||||
"version": "2.2.47",
|
"version": "2.2.57",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "g1nation",
|
"name": "g1nation",
|
||||||
"version": "2.2.47",
|
"version": "2.2.57",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"marked": "^18.0.2"
|
"marked": "^18.0.2"
|
||||||
|
|||||||
+8
-2
@@ -85,7 +85,9 @@ export class AgentExecutor {
|
|||||||
visionContent?: any[],
|
visionContent?: any[],
|
||||||
temperature?: number,
|
temperature?: number,
|
||||||
systemPrompt?: string,
|
systemPrompt?: string,
|
||||||
runId?: number
|
runId?: number,
|
||||||
|
agentSkillContext?: string,
|
||||||
|
negativePrompt?: string
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
@@ -191,7 +193,11 @@ export class AgentExecutor {
|
|||||||
const internetCtx = internetEnabled
|
const internetCtx = internetEnabled
|
||||||
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
|
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
|
||||||
: '';
|
: '';
|
||||||
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${brainContext}\n${contextBlock}`;
|
|
||||||
|
const agentSkillCtx = options.agentSkillContext ? `\n\n[AGENT PERSONA & SKILLS]\n${options.agentSkillContext}` : '';
|
||||||
|
const negativeCtx = options.negativePrompt ? `\n\n[STRICT NEGATIVE PROMPT - DO NOT DO THIS]\n${options.negativePrompt}` : '';
|
||||||
|
|
||||||
|
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${brainContext}\n${contextBlock}${agentSkillCtx}${negativeCtx}`;
|
||||||
const messagesForRequest: ChatMessage[] = [
|
const messagesForRequest: ChatMessage[] = [
|
||||||
{ role: 'system', content: fullSystemPrompt, internal: true },
|
{ role: 'system', content: fullSystemPrompt, internal: true },
|
||||||
...reqMessages
|
...reqMessages
|
||||||
|
|||||||
+108
-3
@@ -89,6 +89,12 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
case 'getModels':
|
case 'getModels':
|
||||||
await this._sendModels();
|
await this._sendModels();
|
||||||
break;
|
break;
|
||||||
|
case 'getAgents':
|
||||||
|
await this._sendAgentsList();
|
||||||
|
break;
|
||||||
|
case 'createAgent':
|
||||||
|
await this._createAgent();
|
||||||
|
break;
|
||||||
case 'newChat':
|
case 'newChat':
|
||||||
this._currentSessionId = null;
|
this._currentSessionId = null;
|
||||||
this._currentSessionBrainId = getActiveBrainProfile().id;
|
this._currentSessionBrainId = getActiveBrainProfile().id;
|
||||||
@@ -560,16 +566,79 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getAgentsDir(): string {
|
||||||
|
const defaultPath = 'E:\\Wiki\\Agent\\.agent\\skills';
|
||||||
|
if (fs.existsSync(defaultPath)) return defaultPath;
|
||||||
|
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (workspaceFolders) {
|
||||||
|
const localPath = path.join(workspaceFolders[0].uri.fsPath, '.agent', 'skills');
|
||||||
|
if (!fs.existsSync(localPath)) {
|
||||||
|
fs.mkdirSync(localPath, { recursive: true });
|
||||||
|
}
|
||||||
|
return localPath;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _sendAgentsList() {
|
||||||
|
if (!this._view) return;
|
||||||
|
const dir = this._getAgentsDir();
|
||||||
|
const agents = [];
|
||||||
|
if (dir && fs.existsSync(dir)) {
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
for (const f of files) {
|
||||||
|
if (f.endsWith('.md')) {
|
||||||
|
agents.push({ name: f.replace('.md', ''), path: path.join(dir, f) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._view.webview.postMessage({ type: 'agentsList', value: agents });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createAgent() {
|
||||||
|
const name = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Name of the new Agent (e.g., frontend_expert)',
|
||||||
|
placeHolder: 'Agent name...'
|
||||||
|
});
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
const safeName = name.trim().replace(/[^a-zA-Z0-9_\\-\\u3131-\\uD79D]/g, '_');
|
||||||
|
if (!safeName) return;
|
||||||
|
|
||||||
|
const dir = this._getAgentsDir();
|
||||||
|
if (!dir) {
|
||||||
|
vscode.window.showErrorMessage('Agent directory could not be determined.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(dir, `${safeName}.md`);
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
fs.writeFileSync(filePath, `# Agent Persona: ${safeName}\\n\\nAdd your instructions here...\\n`, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await vscode.workspace.openTextDocument(filePath);
|
||||||
|
await vscode.window.showTextDocument(doc);
|
||||||
|
await this._sendAgentsList();
|
||||||
|
}
|
||||||
|
|
||||||
private async _handlePrompt(data: any) {
|
private async _handlePrompt(data: any) {
|
||||||
if (!this._view) return;
|
if (!this._view) return;
|
||||||
|
|
||||||
const { value, model, internet, files } = data;
|
const { value, model, internet, files, agentFile, negativePrompt } = data;
|
||||||
this._currentSessionBrainId = getActiveBrainProfile().id;
|
this._currentSessionBrainId = getActiveBrainProfile().id;
|
||||||
|
|
||||||
|
let agentSkillContext = undefined;
|
||||||
|
if (agentFile && fs.existsSync(agentFile)) {
|
||||||
|
agentSkillContext = fs.readFileSync(agentFile, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._agent.handlePrompt(value, model, {
|
await this._agent.handlePrompt(value, model, {
|
||||||
internetEnabled: internet,
|
internetEnabled: internet,
|
||||||
visionContent: files // Agent seems to handle files via visionContent
|
visionContent: files,
|
||||||
|
agentSkillContext,
|
||||||
|
negativePrompt
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logError('Prompt handling failed in sidebar provider.', { error: error?.message || String(error), promptPreview: summarizeText(value || '', 200) });
|
logError('Prompt handling failed in sidebar provider.', { error: error?.message || String(error), promptPreview: summarizeText(value || '', 200) });
|
||||||
@@ -905,6 +974,8 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
<div id="statusDot" style="width:6px; height:6px; border-radius:50%; background:var(--text-dim);"></div>
|
<div id="statusDot" style="width:6px; height:6px; border-radius:50%; background:var(--text-dim);"></div>
|
||||||
<select id="modelSel" title="Select Model"></select>
|
<select id="modelSel" title="Select Model"></select>
|
||||||
<select id="brainSel" title="Select Brain"></select>
|
<select id="brainSel" title="Select Brain"></select>
|
||||||
|
<select id="agentSel" title="Select Agentic Skill"></select>
|
||||||
|
<button class="icon-btn" id="addAgentBtn" title="Create Agent">+</button>
|
||||||
<button class="icon-btn" id="internetBtn" title="Internet Access">🌐</button>
|
<button class="icon-btn" id="internetBtn" title="Internet Access">🌐</button>
|
||||||
<button class="icon-btn" id="brainBtn" title="Sync Knowledge">🧠</button>
|
<button class="icon-btn" id="brainBtn" title="Sync Knowledge">🧠</button>
|
||||||
<button class="icon-btn" id="historyBtn" title="History">📜</button>
|
<button class="icon-btn" id="historyBtn" title="History">📜</button>
|
||||||
@@ -930,6 +1001,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-wrap">
|
<div class="input-wrap">
|
||||||
|
<div id="agentConfigPanel" style="display:none; padding-bottom:8px;">
|
||||||
|
<textarea id="negativePrompt" rows="2" placeholder="Negative Prompt (What NOT to do)..." style="font-size:11.5px; padding:8px; border-radius:8px; border:1px solid var(--border); background:var(--input-bg); color:var(--text-bright); width:100%; resize:vertical; font-family:inherit; outline:none;"></textarea>
|
||||||
|
</div>
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<div id="attachPreview" class="attachment-preview"></div>
|
<div id="attachPreview" class="attachment-preview"></div>
|
||||||
<textarea id="input" rows="1" placeholder="Type your request..."></textarea>
|
<textarea id="input" rows="1" placeholder="Type your request..."></textarea>
|
||||||
@@ -959,6 +1033,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
const attachBtn = document.getElementById('attachBtn');
|
const attachBtn = document.getElementById('attachBtn');
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const attachPreview = document.getElementById('attachPreview');
|
const attachPreview = document.getElementById('attachPreview');
|
||||||
|
const agentSel = document.getElementById('agentSel');
|
||||||
|
const addAgentBtn = document.getElementById('addAgentBtn');
|
||||||
|
const agentConfigPanel = document.getElementById('agentConfigPanel');
|
||||||
|
const negativePrompt = document.getElementById('negativePrompt');
|
||||||
|
|
||||||
let streamBody = null;
|
let streamBody = null;
|
||||||
let internetEnabled = false;
|
let internetEnabled = false;
|
||||||
@@ -1078,6 +1156,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
statusLabel.innerText = msg.value; thinkingBar.classList.add('active');
|
statusLabel.innerText = msg.value; thinkingBar.classList.add('active');
|
||||||
setTimeout(() => { thinkingBar.classList.remove('active'); }, 3000);
|
setTimeout(() => { thinkingBar.classList.remove('active'); }, 3000);
|
||||||
break;
|
break;
|
||||||
|
case 'agentsList':
|
||||||
|
agentSel.innerHTML = '<option value="none">No Agent</option>';
|
||||||
|
msg.value.forEach(a => {
|
||||||
|
const o = document.createElement('option'); o.value = a.path; o.innerText = a.name;
|
||||||
|
agentSel.appendChild(o);
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
thinkingBar.classList.remove('active'); sendBtn.disabled = false;
|
thinkingBar.classList.remove('active'); sendBtn.disabled = false;
|
||||||
addMsg(msg.value, 'error');
|
addMsg(msg.value, 'error');
|
||||||
@@ -1114,7 +1199,15 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
const val = input.value.trim();
|
const val = input.value.trim();
|
||||||
if (!val && pendingFiles.length === 0) return;
|
if (!val && pendingFiles.length === 0) return;
|
||||||
addMsg(val || (pendingFiles.length > 0 ? \`[Sent \${pendingFiles.length} files]\` : ''), 'user');
|
addMsg(val || (pendingFiles.length > 0 ? \`[Sent \${pendingFiles.length} files]\` : ''), 'user');
|
||||||
vscode.postMessage({ type: 'prompt', value: val, model: modelSel.value, internet: internetEnabled, files: pendingFiles.length > 0 ? pendingFiles : undefined });
|
vscode.postMessage({
|
||||||
|
type: 'prompt',
|
||||||
|
value: val,
|
||||||
|
model: modelSel.value,
|
||||||
|
internet: internetEnabled,
|
||||||
|
files: pendingFiles.length > 0 ? pendingFiles : undefined,
|
||||||
|
agentFile: agentSel.value === 'none' ? undefined : agentSel.value,
|
||||||
|
negativePrompt: negativePrompt.value.trim() || undefined
|
||||||
|
});
|
||||||
input.value = ''; input.style.height = 'auto'; pendingFiles = []; renderAttachments();
|
input.value = ''; input.style.height = 'auto'; pendingFiles = []; renderAttachments();
|
||||||
sendBtn.disabled = true; thinkingBar.classList.add('active');
|
sendBtn.disabled = true; thinkingBar.classList.add('active');
|
||||||
}
|
}
|
||||||
@@ -1147,7 +1240,19 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
agentSel.onchange = () => {
|
||||||
|
if (agentSel.value !== 'none') {
|
||||||
|
agentConfigPanel.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
agentConfigPanel.style.display = 'none';
|
||||||
|
negativePrompt.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addAgentBtn.onclick = () => vscode.postMessage({ type: 'createAgent' });
|
||||||
|
|
||||||
vscode.postMessage({ type: 'getModels' });
|
vscode.postMessage({ type: 'getModels' });
|
||||||
|
vscode.postMessage({ type: 'getAgents' });
|
||||||
vscode.postMessage({ type: 'ready' });
|
vscode.postMessage({ type: 'ready' });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# 📋 Task Plan: Agentic Skill & Negative Prompt Integration
|
||||||
|
|
||||||
|
## 1. 개요 (Overview)
|
||||||
|
ConnectAI(G1nation) 익스텐션의 사이드바(Webview)에 'Agentic Skill(자율 에이전트 스킬)' 선택 기능과 'Negative Prompt(금지어/금지행동)' 입력 기능을 추가합니다.
|
||||||
|
사용자가 사이드바에서 직접 새로운 에이전트 프롬프트를 작성하면, 이를 지정된 디렉토리에 Markdown(.md) 파일로 영구 저장하여 유지보수성을 극대화합니다. 생성된 에이전트 스킬과 네거티브 프롬프트는 LLM 질의 시 System Prompt로 동적 병합(Injection)됩니다.
|
||||||
|
|
||||||
|
## 2. 기술 스택 (Tech Stack)
|
||||||
|
- **Frontend (Webview)**: HTML, Vanilla JavaScript, VS Code Webview API, TailwindCSS (기존 스타일링 기준)
|
||||||
|
- **Backend (Extension)**: TypeScript, Node.js `fs` (File System), `path` 모듈
|
||||||
|
- **Data Storage**: 로컬 파일 시스템 (Markdown `.md` 파일)
|
||||||
|
- **Target Path**: `E:\Wiki\Agent\.agent\skills\` (또는 VSCode Workspace 설정에 따른 상대 경로, 기본값으로 활용)
|
||||||
|
|
||||||
|
## 3. UI/UX 상세 설계 (세밀한 텍스트 묘사)
|
||||||
|
사이드바(`sidebarProvider.ts` 내부 HTML 템플릿)의 형태가 다음과 같이 변경/추가됩니다.
|
||||||
|
|
||||||
|
1. **Agentic Skill 섹션**
|
||||||
|
- **라벨**: `Agentic Skill`
|
||||||
|
- **드롭다운 (`<select>`)**: `Local Brain` 선택창 바로 아래에 위치. 기본값은 `None`. `E:\Wiki\Agent\.agent\skills\` 폴더를 스캔하여 `.md` 파일 목록을 옵션으로 렌더링.
|
||||||
|
- **[+ Create Agent] 버튼**: 드롭다운 우측 또는 하단에 위치. 80% 너비의 푸른색 계열 버튼(또는 기존 테마에 맞춘 Accent 버튼). 클릭 시 '새 에이전트 생성' 폼(모달 혹은 토글 영역) 노출.
|
||||||
|
2. **에이전트 생성 폼 (숨김/노출형)**
|
||||||
|
- **Agent Name (`<input>`)**: 에이전트의 이름 (파일명으로 사용됨).
|
||||||
|
- **Agent Prompt (`<textarea>`)**: 에이전트의 역할과 지시사항을 입력하는 넓은 텍스트 에어리어.
|
||||||
|
- **[Save & Select] 버튼**: 저장 후 즉시 드롭다운에서 해당 에이전트를 선택 상태로 변경.
|
||||||
|
3. **Negative Prompt 섹션**
|
||||||
|
- **라벨**: `Negative Prompt (Do NOT do)`
|
||||||
|
- **입력창 (`<textarea>`)**: 프롬프트 창 위에 위치. 3줄 정도 높이의 텍스트 에어리어. 에이전트가 "절대 하지 말아야 할 행동"을 기입.
|
||||||
|
|
||||||
|
## 4. 데이터 플로우 및 ERD (Data Architecture)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant Webview as VSCode Sidebar (Webview)
|
||||||
|
participant ExtHost as Extension (sidebarProvider.ts)
|
||||||
|
participant FS as File System (.md)
|
||||||
|
participant Agent as Agent Logic (agent.ts)
|
||||||
|
|
||||||
|
%% 새 에이전트 생성 흐름
|
||||||
|
User->>Webview: [+ Create Agent] 클릭 및 폼 입력 후 Save
|
||||||
|
Webview->>ExtHost: postMessage({ type: 'createAgent', name, prompt })
|
||||||
|
ExtHost->>FS: fs.writeFileSync(AgentPath/name.md, prompt)
|
||||||
|
ExtHost->>FS: fs.readdirSync(AgentPath)
|
||||||
|
FS-->>ExtHost: 갱신된 파일 목록 반환
|
||||||
|
ExtHost->>Webview: postMessage({ type: 'updateAgentList', agents })
|
||||||
|
Webview-->>User: 드롭다운 업데이트 및 자동 선택
|
||||||
|
|
||||||
|
%% 질의 및 프롬프트 주입 흐름
|
||||||
|
User->>Webview: 채팅 입력 후 Submit
|
||||||
|
Webview->>ExtHost: postMessage({ type: 'ask', value, agentSkill, negativePrompt })
|
||||||
|
ExtHost->>FS: fs.readFileSync(AgentPath/agentSkill.md)
|
||||||
|
FS-->>ExtHost: Agent Markdown 내용 반환
|
||||||
|
ExtHost->>Agent: ask(query, context, agentContent, negativePrompt)
|
||||||
|
Note over Agent: System Prompt 완벽 병합
|
||||||
|
Agent-->>Webview: LLM 응답 스트리밍
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 핵심 로직 명세 (Implementation Details)
|
||||||
|
|
||||||
|
### A. `sidebarProvider.ts` 변경 사항
|
||||||
|
- **메시지 핸들러 추가 (`_webviewView.webview.onDidReceiveMessage`)**:
|
||||||
|
- `getAgents`: 디렉토리 스캔 후 `.md` 파일 목록 반환.
|
||||||
|
- `createAgent`: 파일명 정규화(공백을 `_`로 치환 등) 후 `fs.writeFileSync`를 통해 마크다운 생성. 생성 후 `getAgents` 동작 수행.
|
||||||
|
- **Webview HTML 업데이트**: 새로운 `select`, `textarea`, `button` 추가. JS 로직에서 VS Code API를 통해 메시지 송수신 이벤트 리스너 부착.
|
||||||
|
|
||||||
|
### B. `agent.ts` 변경 사항 (Prompt Injection)
|
||||||
|
- `systemPrompt` 또는 `context` 조립 단계에서 다음 구조로 병합 수행:
|
||||||
|
```text
|
||||||
|
[Base Extension Instructions]
|
||||||
|
...
|
||||||
|
<Local Brain Context (if any)>
|
||||||
|
...
|
||||||
|
# [Agent Persona / Instructions]
|
||||||
|
{{AgentContent (from .md file)}}
|
||||||
|
...
|
||||||
|
# [Negative Prompt / Strict Rules]
|
||||||
|
DO NOT do the following:
|
||||||
|
{{NegativePrompt}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 예상되는 리스크 및 대안 (Pre-mortem)
|
||||||
|
1. **경로 문제 (Unknown Path Risk)**:
|
||||||
|
- *문제*: `E:\Wiki\Agent\.agent\skills` 경로가 다른 환경(Mac 등)이나 설정되지 않은 워크스페이스에서 오류를 발생시킬 수 있음.
|
||||||
|
- *대안*: `fs.existsSync`로 폴더 존재 여부 우선 체크. 폴더가 없으면 에러를 띄우거나, 워크스페이스 루트 내 `.agents` 폴더를 임시로 생성하도록 방어 코드(Fallback) 작성.
|
||||||
|
2. **에이전트 이름 중복 및 특수문자**:
|
||||||
|
- *문제*: 파일 시스템에 부적절한 이름 저장 시 에러.
|
||||||
|
- *대안*: 파일명 저장 시 영문/한글/숫자 외 특수문자 제거 정규식 적용. 중복 시 덮어쓰기 로직 처리.
|
||||||
Reference in New Issue
Block a user