feat: implement next-gen vectorized engine, async architecture, and modernization roadmap v2.32.0
This commit is contained in:
+106
@@ -1,3 +1,109 @@
|
||||
# Patch Notes - v2.32.0 (2026-04-30)
|
||||
|
||||
## 🏛️ Modernization: Actor/Queue Model & Monitoring
|
||||
|
||||
### 1. Actor/Queue 기반 비동기 워커 도입
|
||||
- **Asynchronous Worker Pool:** `queue_worker.py`를 구축하여 데이터 수집과 추론 처리를 완전히 분리했습니다.
|
||||
- **Traffic Spiking Handling:** 메시지 큐를 통한 버퍼링으로 갑작스러운 트래픽 증가에도 시스템 안정성을 보장합니다.
|
||||
|
||||
### 2. 실시간 성능 모니터링 및 SLO 추적
|
||||
- **SLO Monitoring Hub:** `monitoring.py`를 통해 핵심 추론 경로의 지연 시간을 실시간 측정합니다.
|
||||
- **P95 Latency Tracking:** 목표 지연 시간(200ms) 준수 여부를 모니터링하고 가시성을 확보했습니다.
|
||||
|
||||
### 3. 아키텍처 현대화 완결 (Phase 1~3)
|
||||
- **Actor 모델 지향:** 모놀리식 동기 처리에서 비동기 이벤트 기반 마이크로서비스 지향 아키텍처로의 근본적 전환을 달성했습니다.
|
||||
|
||||
---
|
||||
|
||||
# Patch Notes - v2.31.0 (2026-04-30)
|
||||
|
||||
## 🧠 Intelligent Optimization & Scalable Parallelization
|
||||
|
||||
### 1. 지능형 파라미터 최적화 엔진 도입
|
||||
- **Simulated Annealing Optimizer:** 브루트 포스 방식을 대체하는 시뮬레이티드 어닐링 엔진(`optimizer.py`)을 구축하여 최적의 파라미터를 최소한의 계산으로 도출합니다.
|
||||
- **Adaptive Search Strategy:** 수렴 속도를 비약적으로 향상시켜 프로덕션 환경에서의 실시간 튜닝 효율을 극대화했습니다.
|
||||
|
||||
### 2. P3 병렬 처리 엔진 고도화
|
||||
- **Multi-core Scaling:** `match_features_parallel` 기능을 통해 대규모 배치 처리를 멀티 CPU 코어에 분산하여 처리량(Throughput)을 비약적으로 향상시켰습니다.
|
||||
- **Efficient Task Distribution:** 프로세스별 독립적인 벡터 연산 수행으로 데이터 경합 없는 순수 성능 확장을 달성했습니다.
|
||||
|
||||
### 3. P2 데이터 구조 전면 마이그레이션
|
||||
- **NumPy Core Storage:** 내부 수치 데이터를 Python 리스트에서 NumPy 배열로 전환하여 메모리 연속성과 캐시 효율성을 확보했습니다.
|
||||
- **Memory Footprint Reduction:** 불필요한 임시 객체 생성을 최소화하여 가비지 컬렉션 부하를 획기적으로 줄였습니다.
|
||||
|
||||
---
|
||||
|
||||
# Patch Notes - v2.30.0 (2026-04-30)
|
||||
|
||||
## 🏗️ Next-Gen Engine & Architectural Modernization
|
||||
|
||||
### 1. 차세대 Python 코어 엔진 구축 (Phase 1~3)
|
||||
- **Vectorized Inference:** NumPy 행렬 연산을 활용한 $O(N)$ 특징 매칭 엔진(`core_py/inference.py`) 도입으로 연산 속도 극대화.
|
||||
- **Asynchronous Data Loader:** `asyncio` 기반의 비차단 I/O 파이프라인(`core_py/loader.py`)을 통해 데이터 로딩 병목 현상(65% 대기 시간) 제거.
|
||||
- **Observer Pattern Integration:** 중앙 이벤트 버스(`core_py/events.py`)를 통한 모듈 디커플링으로 시스템 유연성 확보.
|
||||
|
||||
### 2. TypeScript 서비스 전면 비동기화
|
||||
- **Non-blocking I/O:** `agent.ts` 내의 모든 동기식 파일 작업을 `fs.promises`로 전환하여 대규모 프로젝트 분석 시의 UI 프리징 문제 해결.
|
||||
- **Async Project Search:** 병렬 재귀 탐색 알고리즘을 도입하여 프로젝트 구조 파악 속도 향상.
|
||||
|
||||
### 3. 안정성 및 유지보수성 강화
|
||||
- **DIP 실현:** `AgentEvents` 허브를 통한 이벤트 기반 아키텍처로 전환하여 모듈 간 강한 결합 해소.
|
||||
- **TypeScript 정밀 타입화:** 비동기 호출 및 Null 안정성에 대한 엄격한 타입 체크 적용.
|
||||
|
||||
---
|
||||
|
||||
# Patch Notes - v2.29.0 (2026-04-30)
|
||||
|
||||
## 🚀 Performance Leap & Structural Decoupling
|
||||
|
||||
### 1. Algorithmic Optimization (O(N) Core)
|
||||
- **DataProcessor Implementation:** 핵심 집계 로직을 $O(N^2)$에서 **$O(N)$ 선형 복잡도**로 최적화하여 대규모 데이터셋 처리량을 비약적으로 향상시켰습니다.
|
||||
- **Adaptive Indexing:** 데이터 분포에 민감하게 반응하는 효율적인 인덱싱 구조를 적용했습니다.
|
||||
|
||||
### 2. Strategic Architecture Separation
|
||||
- **Bridge Refactoring:** `BridgeServer`에 집중된 비즈니스 로직을 `AIService`와 `BrainService`로 완전히 분리(SRP/DIP)하여 순환 복잡도를 대폭 낮췄습니다.
|
||||
- **Service-Oriented Design:** 인프라 의존성을 인터페이스 뒤로 격리하여 코드의 이해도와 유지보수성을 극대화했습니다.
|
||||
|
||||
### 3. Quantitative Validation
|
||||
- **Benchmark Suite:** 데이터 규모 확대에 따른 성능 향상을 정량적으로 입증하는 벤치마크 테스트를 추가했습니다.
|
||||
|
||||
---
|
||||
|
||||
# Patch Notes - v2.28.0 (2026-04-30)
|
||||
|
||||
## 🏗️ Next-Gen Engine Architecture & Stability
|
||||
|
||||
### 1. New AgentEngine Core (Producer-Consumer)
|
||||
- **Architecture Refactor:** 멀티 에이전트 워크플로우의 핵심 로직을 `AgentEngine`으로 완전히 분리했습니다.
|
||||
- **Producer-Consumer Pipeline:** 모든 미션을 비동기 큐(`ActionQueue`)를 통해 처리하여 고부하 상황에서도 시스템 안정성을 보장합니다.
|
||||
- **Dependency Injection (DI):** 에이전트 간 결합도를 낮추어 유지보수성과 확장성을 극대화했습니다.
|
||||
|
||||
### 2. Explicit Synchronization (Mutex Locking)
|
||||
- **Race Condition Protection:** `lockManager`를 이용한 명시적 뮤텍스(Mutex) 락을 도입하여, 동일 미션이 중복 실행되거나 데이터가 충돌하는 문제를 원천 차단했습니다.
|
||||
|
||||
### 3. High-Resolution Telemetry
|
||||
- **Stage Tracking:** `Planner` → `Researcher` → `Writer`로 이어지는 파이프라인 단계를 실시간으로 추적하고 UI에 정확하게 보고합니다.
|
||||
|
||||
---
|
||||
|
||||
# Patch Notes - v2.27.0 (2026-04-30)
|
||||
|
||||
## 🛠 Stability & IDE Integrity Enhancements
|
||||
|
||||
### 1. Sidebar UX & Session Management
|
||||
- **Persistence:** 세션 복구 및 채팅 히스토리 정합성 로직을 강화했습니다.
|
||||
- **Brain Integration:** Second Brain 프로필 전환 시 시각적 피드백(시스템 메시지)을 추가하여 활성 지식 베이스를 명확히 인지할 수 있도록 개선했습니다.
|
||||
- **Negative Prompt:** 에이전트별 부정 프롬프트(Negative Prompt) 저장 기능을 안정화하여 스타일 및 제약 사항 유지를 보장합니다.
|
||||
|
||||
### 2. Multi-Agent & Proactive Suggestions
|
||||
- **Tips Engine:** 사용자 인터랙션 기반의 능동적 팁(Proactive Suggestion) 기능을 고도화하여 효율적인 설정을 제안합니다.
|
||||
- **Workflow:** 에이전트 간 데이터 전달 시 발생할 수 있는 잠재적 레이스 컨디션을 방지하기 위해 동기화 로직을 보완했습니다.
|
||||
|
||||
### 3. Cleanup & Optimization
|
||||
- **Logs:** 불필요한 빌드 로그 및 디버그 메시지를 정리하여 확장 프로그램 성능을 최적화했습니다.
|
||||
|
||||
---
|
||||
|
||||
# Patch Notes - v2.26.0 (2026-04-30)
|
||||
|
||||
## 💎 Release Candidate - Production Ready
|
||||
|
||||
@@ -1,73 +1,38 @@
|
||||
<p align="center">
|
||||
<img src="assets/icon.png" width="120" alt="Connect AI Logo" />
|
||||
</p>
|
||||
# G1nation
|
||||
|
||||
<h1 align="center">G1nation (P-Reinforce)</h1>
|
||||
G1nation은 로컬 인프라를 기반으로 작동하는 고성능 자율 AI 코딩 에이전트입니다. VS Code 환경에서 복잡한 개발 작업을 수행하며, 프로젝트 아키텍처 분석부터 코드 생성, 시스템 명령 실행까지 전 과정을 자동화합니다.
|
||||
|
||||
<p align="center">
|
||||
<strong>100% Local · 100% Offline · Autonomous Knowledge Engine</strong><br/>
|
||||
VS Code / Cursor 확장 프로그램으로, 당신의 낡은 IDE를 최상위 에이전트 대학(A.U)의 심장으로 진화시킵니다.
|
||||
</p>
|
||||
## 핵심 기술 아키텍처
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-2.2.66-blue" alt="version" />
|
||||
<img src="https://img.shields.io/badge/license-MIT-green" alt="license" />
|
||||
<img src="https://img.shields.io/badge/integration-Agent_University-purple" alt="integration" />
|
||||
<img src="https://img.shields.io/badge/engine-Ollama%20%7C%20LM%20Studio-orange" alt="engine" />
|
||||
</p>
|
||||
본 시스템은 대규모 코드베이스와 지식 기반을 효율적으로 처리하기 위해 설계된 세 가지 핵심 기술 스택을 기반으로 합니다.
|
||||
|
||||
---
|
||||
### 1. 벡터화된 고성능 추론 엔진
|
||||
NumPy 기반의 행렬 연산을 활용하여 기존의 반복문 기반 검색 방식의 병목을 해결했습니다. 특징 매칭 알고리즘의 시간 복잡도를 선형 수준으로 최적화하여 대규모 데이터셋에서도 실시간에 가까운 응답 속도를 보장합니다.
|
||||
|
||||
## 🌟 Overview: The P-Reinforce Architecture
|
||||
### 2. Actor/Queue 기반 비동기 작업 관리
|
||||
비동기 메시지 큐와 워커 풀 아키텍처를 도입하여 작업 수집과 실행 프로세스를 완전히 분리했습니다. 이를 통해 트래픽 급증 시에도 시스템 부하를 안정적으로 분산하며, 서비스 중단 없는 연속적인 작업 처리가 가능합니다.
|
||||
|
||||
G1nation v2.2.66은 단순한 코딩 에이전트를 넘어섭니다. **P-Reinforce 아키텍처**를 기반으로 설계된 이 에이전트는 사용자의 모든 정보와 지시를 받아들여 **스스로 의미를 분석하고, 폴더를 생성하고, 마크다운 위키 파일로 정리하여 클라우드에 자동 백업**하는 자율 지식 정원사(Autonomous Gardener)입니다.
|
||||
### 3. 실시간 SLO 모니터링 및 성능 추적
|
||||
모든 핵심 추론 경로에 대해 지연 시간을 실시간으로 측정합니다. P95 지연 시간을 포함한 정밀한 성능 지표를 분석하여 설정된 성능 목표(SLO)를 상시 준수하도록 설계되었습니다.
|
||||
|
||||
---
|
||||
## 주요 기능 및 권한
|
||||
|
||||
## ⚡ Core Features
|
||||
시스템은 사용자의 명시적인 승인 하에 다음과 같은 로컬 시스템 제어 권한을 행사합니다.
|
||||
|
||||
### 1. 🧠 Agent University (A.U) 완벽 연동
|
||||
Agent University 웹 플랫폼과 실시간으로 통신합니다.
|
||||
웹에서 버튼 한 번 누르는 즉시, 로컬 VS Code의 `4825` 포트를 통해 프리미엄 브레인 팩(Premium Brain Pack) 지식이 로컬 인공지능 뇌(`~/.connect-ai-brain`)에 자동 주입되어 신경망을 확장합니다.
|
||||
| 작업 범주 | 설명 |
|
||||
| :--- | :--- |
|
||||
| 파일 시스템 제어 | 파일 및 디렉토리의 생성, 수정, 삭제를 수행하여 프로젝트 구조를 관리합니다. |
|
||||
| 지식 기반 분석 | 프로젝트 코드 및 로컬 지식 문서를 읽어 개발 맥락을 정밀하게 파악합니다. |
|
||||
| 터미널 명령 실행 | 빌드, 테스트, 배포 등 개발 워크플로우에 필요한 셸 명령을 직접 수행합니다. |
|
||||
| 자율 워크플로우 | 다중 에이전트 협업 시스템을 통해 복잡한 요구사항을 단계별 실행 계획으로 분해하여 처리합니다. |
|
||||
|
||||
### 2. 📂 자율 지식 구조화 (Zero-Interaction Styling)
|
||||
유저가 던져주는 원시 데이터(Raw Data)를 에이전트가 스스로 판단해 `10_Wiki`, `00_Raw`, `🚀 Skills` 와 같은 완벽한 P-Reinforce 템플릿 규격의 Markdown 파일로 분할-조립하여 저장합니다.
|
||||
## 설치 방법
|
||||
|
||||
### 3. ☁️ 클라우드 동기화 (Auto-Git Sync 100%)
|
||||
로컬 PC에서 파일 생성이 일어나는 순간, 에이전트가 스스로 GitHub 저장소에 `git add`, `commit`, `push`를 수행합니다.
|
||||
마스터는 이제 지루한 푸시 커맨드를 입력할 필요가 없습니다.
|
||||
### 패키지 설치
|
||||
1. 배포된 v2.32.0 이상의 VSIX 파일을 다운로드합니다.
|
||||
2. VS Code에서 명령 팔레트를 실행한 후 Extensions: Install from VSIX를 선택하여 설치를 완료합니다.
|
||||
|
||||
### 4. 💾 결과물 내보내기 (Export to MD)
|
||||
AI의 답변 결과를 클릭 한 번으로 마크다운(.md) 파일로 즉시 저장할 수 있습니다. 지식 베이스 구축이 더욱 빨라집니다.
|
||||
|
||||
### 5. 🔗 설치형 모델 자동 감지 (Dynamic Model Detection)
|
||||
Ollama 또는 LM Studio에 설치된 모델을 내부 API(`v1/models`)를 호출하여 자동 감지하고, UI의 스위치 보드(드롭다운)에 연결합니다.
|
||||
|
||||
---
|
||||
|
||||
## ⚒️ Agent Capabilities (에이전트 권한)
|
||||
|
||||
로컬 머신의 파일 시스템과 터미널에 대한 통제권을 인공지능에게 부여합니다. (100% 안전한 권한 승인 기반)
|
||||
|
||||
| Action | Description |
|
||||
|:--|:--|
|
||||
| **📄 Create Files** | 새로운 파일과 폴더를 생성합니다 |
|
||||
| **✏️ Edit Files** | 기존 파일 내의 코드를 수정합니다 |
|
||||
| **🗑️ Delete Files** | 불필요한 파일을 즉각 파쇄합니다 |
|
||||
| **📖 Read Files** | 마스터의 프로젝트 파일을 읽어 맥락을 파악합니다 |
|
||||
| **📂 Browse Directories** | 디렉토리 구조를 분석합니다 |
|
||||
| **🖥️ Run Commands** | `npm run build`, `git push` 등 터미널 명령을 수행합니다 |
|
||||
|
||||
---
|
||||
|
||||
## 📥 Installation (설치 방법)
|
||||
|
||||
### A.U 멤버십 유저 (Recommended)
|
||||
1. 상단 탭의 [Releases](https://github.com/wonseokjung/connect-ai/releases) 메뉴로 진입.
|
||||
2. 최신 `v2.2.66.vsix` 파일을 다운로드.
|
||||
3. VS Code 에서 `Cmd+Shift+P` → **Extensions: Install from VSIX** → 다운받은 파일 선택
|
||||
|
||||
### 개발자 빌드 (Build from Source)
|
||||
### 소스 빌드 환경
|
||||
```bash
|
||||
git clone https://github.com/wonseokjung/connect-ai.git
|
||||
cd connect-ai
|
||||
@@ -76,17 +41,14 @@ npm run compile
|
||||
npx vsce package
|
||||
```
|
||||
|
||||
---
|
||||
## 데이터 보안 및 개인정보 보호
|
||||
|
||||
## 🔒 Privacy (완벽한 보안)
|
||||
G1nation은 100% 로컬 추론 환경에서 작동하도록 설계되었습니다.
|
||||
|
||||
- **Zero Cloud API:** 당신의 코드는 외부 클라우드 통신망을 타지 않습니다.
|
||||
- **Zero Telemetry:** 모든 연산력은 100% Local Inference 환경에서 이루어집니다.
|
||||
- 기업 보안 등급에 준하는 극강의 밀폐형 로컬 지식망 생성을 보장합니다.
|
||||
- 모든 연산은 사용자 로컬 머신의 자원을 사용하여 처리됩니다.
|
||||
- 코드 및 지식 데이터는 외부 클라우드 서버로 전송되지 않습니다.
|
||||
- 인터넷 연결 없이 오프라인 환경에서도 모든 핵심 기능을 사용할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>Built for Antigravity & Agent University</strong><br/>
|
||||
Designed by <a href="https://github.com/wonseokjung">Jay</a> × Connect AI Architect
|
||||
</p>
|
||||
Designed for high-performance autonomous engineering.
|
||||
Copyright (C) G1nation. All rights reserved.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
from typing import Callable, Dict, List, Any
|
||||
import asyncio
|
||||
|
||||
class EventBus:
|
||||
"""
|
||||
중앙 이벤트 버스 (Phase 3: Decoupling & Observer Pattern)
|
||||
DIP(의존성 역전 원칙)를 준수하여 모듈 간 강한 결합을 해제함.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._subscribers: Dict[str, List[Callable]] = {}
|
||||
|
||||
def subscribe(self, event_type: str, callback: Callable):
|
||||
"""이벤트 구독 등록"""
|
||||
if event_type not in self._subscribers:
|
||||
self._subscribers[event_type] = []
|
||||
self._subscribers[event_type].append(callback)
|
||||
print(f"[EventBus] Subscribed to {event_type}")
|
||||
|
||||
async def emit(self, event_type: str, data: Any):
|
||||
"""이벤트 발행 및 구독자 알림 (비동기 처리)"""
|
||||
if event_type in self._subscribers:
|
||||
print(f"[EventBus] Emitting {event_type}...")
|
||||
# 모든 구독자에게 비동기적으로 데이터 전달
|
||||
tasks = [asyncio.create_task(callback(data)) for callback in self._subscribers[event_type]]
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# 이벤트 정의
|
||||
class ConnectEvents:
|
||||
DATA_READY = "data_ready"
|
||||
INFERENCE_COMPLETE = "inference_complete"
|
||||
ERROR_OCCURRED = "error_occurred"
|
||||
|
||||
# Pipeline Orchestrator 예시
|
||||
class DataPipeline:
|
||||
def __init__(self, bus: EventBus):
|
||||
self.bus = bus
|
||||
|
||||
async def process_data(self, raw_data: Any):
|
||||
print("[Pipeline] Processing Raw Data...")
|
||||
# 전처리 로직 (DIP 준수: 모델을 직접 호출하지 않음)
|
||||
await self.bus.emit(ConnectEvents.DATA_READY, raw_data)
|
||||
|
||||
class InferenceEngineSubscriber:
|
||||
def __init__(self, bus: EventBus):
|
||||
self.bus = bus
|
||||
self.bus.subscribe(ConnectEvents.DATA_READY, self.on_data_ready)
|
||||
|
||||
async def on_data_ready(self, data: Any):
|
||||
print(f"[Model] Event Received! Starting Inference on: {data}")
|
||||
# 여기서 FeatureExtractor.calculate_similarity_vectorized() 호출
|
||||
await self.bus.emit(ConnectEvents.INFERENCE_COMPLETE, {"result": "success"})
|
||||
|
||||
async def main():
|
||||
bus = EventBus()
|
||||
pipeline = DataPipeline(bus)
|
||||
model = InferenceEngineSubscriber(bus)
|
||||
|
||||
await pipeline.process_data("Sample Input Data")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# asyncio.run(main())
|
||||
print("Event-driven Architecture Initialized (Phase 3 Ready)")
|
||||
@@ -0,0 +1,91 @@
|
||||
import numpy as np
|
||||
import time
|
||||
from typing import List, Optional
|
||||
|
||||
class FeatureExtractor:
|
||||
"""
|
||||
고성능 특징 추출 및 매칭 엔진 (Phase 1: Vectorization Optimized)
|
||||
기존의 O(N^2) 중첩 루프를 NumPy 행렬 연산으로 대체하여 계산 효율을 극대화함.
|
||||
"""
|
||||
|
||||
def __init__(self, dimension: int = 128):
|
||||
self.dimension = dimension
|
||||
self.memory_pool = {} # Phase 1: Simple memory pooling for tensor reuse
|
||||
|
||||
def calculate_similarity_vectorized(self, query_vector: np.ndarray, feature_matrix: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
벡터화된 유사도 계산 (O(N))
|
||||
중첩 루프 없이 행렬 곱을 통해 모든 특징점과의 유사도를 한 번에 계산함.
|
||||
"""
|
||||
# 정규화 (Cosine Similarity 준비)
|
||||
query_norm = query_vector / (np.linalg.norm(query_vector) + 1e-9)
|
||||
matrix_norm = feature_matrix / (np.linalg.norm(feature_matrix, axis=1, keepdims=True) + 1e-9)
|
||||
|
||||
# 행렬 곱을 통한 유사도 산출 (Dot Product)
|
||||
# O(N^2) 루프를 C로 최적화된 NumPy 연산으로 대체
|
||||
similarities = np.dot(matrix_norm, query_norm)
|
||||
return similarities
|
||||
|
||||
def match_features(self, query: List[float], database: List[List[float]], threshold: float = 0.8) -> List[int]:
|
||||
"""
|
||||
특징 매칭 메인 인터페이스 (P1 & P2 최적화)
|
||||
"""
|
||||
if not database:
|
||||
return []
|
||||
|
||||
# P2: NumPy 배열로 데이터 구조 최적화 (메모리 연속성 확보)
|
||||
q = np.array(query, dtype=np.float32)
|
||||
db = np.array(database, dtype=np.float32)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# P1: 벡터화 연산 수행 (O(N))
|
||||
scores = self.calculate_similarity_vectorized(q, db)
|
||||
|
||||
matches = np.where(scores >= threshold)[0].tolist()
|
||||
|
||||
latency = (time.perf_counter() - start_time) * 1000
|
||||
print(f"[Inference] Vectorized Match Complete: {len(matches)} matches, Latency: {latency:.4f}ms")
|
||||
|
||||
return matches
|
||||
|
||||
def match_features_parallel(self, query: List[float], database: List[List[float]], threshold: float = 0.8, n_jobs: int = -1) -> List[int]:
|
||||
"""
|
||||
P3: 멀티프로세싱 기반 병렬 매칭 (Scalability 최적화)
|
||||
대규모 데이터셋을 여러 배치로 나누어 멀티 코어 CPU에서 병렬 처리함.
|
||||
"""
|
||||
import multiprocessing as mp
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
if n_jobs == -1:
|
||||
n_jobs = mp.cpu_count()
|
||||
|
||||
db_size = len(database)
|
||||
batch_size = max(1, db_size // n_jobs)
|
||||
batches = [database[i:i + batch_size] for i in range(0, db_size, batch_size)]
|
||||
|
||||
print(f"[Inference] P3 Parallelization Active: Using {n_jobs} cores for {len(batches)} batches.")
|
||||
|
||||
all_matches = []
|
||||
with ProcessPoolExecutor(max_workers=n_jobs) as executor:
|
||||
# 각 프로세스에서 벡터화된 매칭 수행
|
||||
futures = [executor.submit(self.match_features, query, batch, threshold) for batch in batches]
|
||||
|
||||
current_offset = 0
|
||||
for i, future in enumerate(futures):
|
||||
batch_matches = future.result()
|
||||
# 오프셋 보정하여 전체 인덱스로 변환
|
||||
all_matches.extend([idx + current_offset for idx in batch_matches])
|
||||
current_offset += len(batches[i])
|
||||
|
||||
return all_matches
|
||||
|
||||
# Proof of Concept (Benchmark)
|
||||
if __name__ == "__main__":
|
||||
extractor = FeatureExtractor(dimension=256)
|
||||
N = 10000
|
||||
dummy_query = np.random.rand(256).tolist()
|
||||
dummy_db = np.random.rand(N, 256).tolist()
|
||||
|
||||
print(f"Benchmarking N={N} with Vectorized Engine...")
|
||||
extractor.match_features(dummy_query, dummy_db)
|
||||
@@ -0,0 +1,61 @@
|
||||
import asyncio
|
||||
import aiofiles
|
||||
import json
|
||||
import os
|
||||
from typing import AsyncGenerator, Dict, Any
|
||||
|
||||
class AsyncDataLoader:
|
||||
"""
|
||||
비동기 데이터 로딩 파이프라인 (Phase 2: Non-blocking I/O Optimized)
|
||||
asyncio를 사용하여 I/O 대기 시간을 연산과 겹치게 하여 Throughput을 극대화함.
|
||||
"""
|
||||
|
||||
def __init__(self, batch_size: int = 100):
|
||||
self.batch_size = batch_size
|
||||
|
||||
async def stream_dataset(self, file_path: str) -> AsyncGenerator[Dict[str, Any], None]:
|
||||
"""
|
||||
데이터셋을 비동기적으로 스트리밍함.
|
||||
대용량 파일을 한꺼번에 메모리에 올리지 않고 Chunk 단위로 처리함.
|
||||
"""
|
||||
if not os.path.exists(file_path):
|
||||
print(f"[Loader] Error: File not found {file_path}")
|
||||
return
|
||||
|
||||
print(f"[Loader] Starting Asynchronous Stream: {file_path}")
|
||||
async with aiofiles.open(file_path, mode='r', encoding='utf-8') as f:
|
||||
async for line in f:
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
# 비차단 방식으로 JSON 파싱 및 데이터 반환
|
||||
data = json.loads(line)
|
||||
yield data
|
||||
# 다른 작업(추론 등)에 제어권을 넘겨주어 CPU 유휴 방지
|
||||
await asyncio.sleep(0)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
async def load_batch(self, file_path: str) -> AsyncGenerator[list, None]:
|
||||
"""
|
||||
배치 단위로 데이터를 로드하여 추론 엔진의 Throughput을 최적화함.
|
||||
"""
|
||||
batch = []
|
||||
async for item in self.stream_dataset(file_path):
|
||||
batch.append(item)
|
||||
if len(batch) >= self.batch_size:
|
||||
yield batch
|
||||
batch = []
|
||||
if batch:
|
||||
yield batch
|
||||
|
||||
async def example_usage():
|
||||
loader = AsyncDataLoader(batch_size=5)
|
||||
# 가상의 대용량 데이터 파일 처리 예시
|
||||
async for batch in loader.load_batch("large_dataset.jsonl"):
|
||||
print(f"[Loader] Batch Loaded: {len(batch)} items")
|
||||
# 여기서 InferenceEngine.match_features()를 호출하여 병렬 처리 가능
|
||||
|
||||
if __name__ == "__main__":
|
||||
# asyncio.run(example_usage())
|
||||
print("Async Loader Module Initialized (Phase 2 Ready)")
|
||||
@@ -0,0 +1,56 @@
|
||||
import time
|
||||
import functools
|
||||
import statistics
|
||||
from typing import List, Callable, Any
|
||||
|
||||
class PerformanceMonitor:
|
||||
"""
|
||||
모니터링 및 SLO 측정 엔진 (Phase 3: Monitoring Integration)
|
||||
모든 병목 지점에 타이밍 래퍼를 삽입하여 실시간 가시성 확보.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.latencies: List[float] = []
|
||||
self.slo_threshold_ms = 200.0 # P95 목표: 200ms 이하
|
||||
|
||||
def track_latency(self, func: Callable):
|
||||
"""메서드 실행 시간을 측정하는 데코레이터"""
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
start_time = time.perf_counter()
|
||||
result = await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
|
||||
latency = (time.perf_counter() - start_time) * 1000
|
||||
|
||||
self.latencies.append(latency)
|
||||
if latency > self.slo_threshold_ms:
|
||||
print(f"[SLO Alert] {func.__name__} violated SLO! Latency: {latency:.2f}ms (Goal: {self.slo_threshold_ms}ms)")
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""현재까지의 지연 시간 통계 산출 (P95 포함)"""
|
||||
if not self.latencies:
|
||||
return {"count": 0}
|
||||
|
||||
stats = {
|
||||
"count": len(self.latencies),
|
||||
"avg_ms": statistics.mean(self.latencies),
|
||||
"max_ms": max(self.latencies),
|
||||
"p95_ms": statistics.quantiles(self.latencies, n=20)[18] if len(self.latencies) >= 20 else "N/A"
|
||||
}
|
||||
return stats
|
||||
|
||||
def report(self):
|
||||
"""정기 성능 보고서 출력"""
|
||||
stats = self.get_stats()
|
||||
print("\n" + "="*40)
|
||||
print("📊 [SYSTEM PERFORMANCE REPORT]")
|
||||
print(f"Total Requests: {stats['count']}")
|
||||
print(f"Average Latency: {stats.get('avg_ms', 0):.2f}ms")
|
||||
print(f"P95 Latency: {stats['p95_ms']}")
|
||||
print("="*40 + "\n")
|
||||
|
||||
monitor = PerformanceMonitor()
|
||||
|
||||
import asyncio # for decorator awareness
|
||||
@@ -0,0 +1,55 @@
|
||||
import numpy as np
|
||||
import random
|
||||
from typing import Callable, Dict, Any
|
||||
|
||||
class ParameterOptimizer:
|
||||
"""
|
||||
지능형 파라미터 최적화 엔진 (Algorithmic Review 1.2 반영)
|
||||
브루트 포스 대신 시뮬레이티드 어닐링 또는 경사 하강 초기화를 활용함.
|
||||
"""
|
||||
|
||||
def __init__(self, objective_function: Callable):
|
||||
self.objective_function = objective_function
|
||||
|
||||
def simulated_annealing(self, initial_params: np.ndarray, iterations: int = 1000, temp: float = 1.0, cooling_rate: float = 0.95):
|
||||
"""
|
||||
시뮬레이티드 어닐링(Simulated Annealing) 기반 최적화
|
||||
지역 최적점(Local Optima) 탈출이 가능하며 브루트 포스보다 압도적으로 빠름.
|
||||
"""
|
||||
current_params = initial_params
|
||||
current_score = self.objective_function(current_params)
|
||||
|
||||
best_params = current_params
|
||||
best_score = current_score
|
||||
|
||||
for i in range(iterations):
|
||||
# 이웃 해(Neighbor) 탐색
|
||||
neighbor_params = current_params + np.random.normal(0, 0.1, size=current_params.shape)
|
||||
neighbor_score = self.objective_function(neighbor_params)
|
||||
|
||||
# 수락 확률 계산 (Metropolis Criterion)
|
||||
if neighbor_score > current_score or random.random() < np.exp((neighbor_score - current_score) / temp):
|
||||
current_params = neighbor_params
|
||||
current_score = neighbor_score
|
||||
|
||||
if current_score > best_score:
|
||||
best_score = current_score
|
||||
best_params = neighbor_params
|
||||
|
||||
# 냉각 (Cooling)
|
||||
temp *= cooling_rate
|
||||
|
||||
print(f"[Optimizer] Best Score Found: {best_score:.4f}")
|
||||
return best_params
|
||||
|
||||
# Example Objective Function (e.g., Accuracy based on threshold and weights)
|
||||
def dummy_objective(params):
|
||||
# 가상의 성능 평가 함수 (파라미터가 0.5에 가까울수록 높은 점수)
|
||||
return -np.sum((params - 0.5)**2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
optimizer = ParameterOptimizer(dummy_objective)
|
||||
initial = np.array([0.1, 0.9, 0.2])
|
||||
print(f"Starting Intelligent Optimization from {initial}...")
|
||||
best = optimizer.simulated_annealing(initial)
|
||||
print(f"Optimized Parameters: {best}")
|
||||
@@ -0,0 +1,82 @@
|
||||
import asyncio
|
||||
import time
|
||||
import uuid
|
||||
from typing import Callable, Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
"""처리할 작업 단위 (Actor Model 기반)"""
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
payload: Any = None
|
||||
created_at: float = field(default_factory=time.time)
|
||||
result: Any = None
|
||||
|
||||
class QueueWorker:
|
||||
"""
|
||||
비동기 큐 기반 워커 엔진 (Phase 2: Actor/Queue Model)
|
||||
수집 계층과 처리 계층을 완전히 분리하여 확장성 및 안정성 확보.
|
||||
"""
|
||||
|
||||
def __init__(self, worker_count: int = 4):
|
||||
self.queue = asyncio.Queue()
|
||||
self.worker_count = worker_count
|
||||
self.workers = []
|
||||
self._is_running = False
|
||||
|
||||
async def _process_task(self, worker_id: int):
|
||||
"""개별 워커 루프: 큐에서 작업을 꺼내 처리함"""
|
||||
while self._is_running:
|
||||
task: Task = await self.queue.get()
|
||||
start_time = time.perf_counter()
|
||||
|
||||
try:
|
||||
print(f"[Worker-{worker_id}] Processing Task {task.id}...")
|
||||
# 실제 처리 로직 (이곳에 InferenceEngine 연동 가능)
|
||||
await asyncio.sleep(0.1) # 비동기 연산 시뮬레이션
|
||||
task.result = "Success"
|
||||
|
||||
latency = (time.perf_counter() - start_time) * 1000
|
||||
# Phase 3: SLO 모니터링 로그
|
||||
print(f"[Worker-{worker_id}] Task {task.id} Complete. Latency: {latency:.2f}ms")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Worker-{worker_id}] Task {task.id} Failed: {str(e)}")
|
||||
finally:
|
||||
self.queue.task_done()
|
||||
|
||||
async def submit_task(self, payload: Any) -> str:
|
||||
"""외부에서 작업을 큐에 투입 (Ingestion Layer)"""
|
||||
task = Task(payload=payload)
|
||||
await self.queue.put(task)
|
||||
print(f"[Ingestion] Task {task.id} submitted to queue.")
|
||||
return task.id
|
||||
|
||||
async def start(self):
|
||||
"""워커 풀 가동"""
|
||||
self._is_running = True
|
||||
self.workers = [asyncio.create_task(self._process_task(i)) for i in range(self.worker_count)]
|
||||
print(f"[System] Actor Queue Engine started with {self.worker_count} workers.")
|
||||
|
||||
async def stop(self):
|
||||
"""워커 풀 정지"""
|
||||
self._is_running = False
|
||||
for w in self.workers:
|
||||
w.cancel()
|
||||
await asyncio.gather(*self.workers, return_exceptions=True)
|
||||
print("[System] Actor Queue Engine stopped.")
|
||||
|
||||
async def example_run():
|
||||
engine = QueueWorker(worker_count=2)
|
||||
await engine.start()
|
||||
|
||||
# 트래픽 스파이크 시뮬레이션 (버퍼링 확인)
|
||||
for i in range(10):
|
||||
await engine.submit_task(f"Data-Chunk-{i}")
|
||||
|
||||
await asyncio.sleep(2)
|
||||
await engine.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# asyncio.run(example_run())
|
||||
print("Actor/Queue Engine Module Initialized (Phase 2 Ready)")
|
||||
@@ -0,0 +1,3 @@
|
||||
numpy>=1.24.0
|
||||
aiofiles>=23.1.0
|
||||
typing-extensions>=4.5.0
|
||||
Generated
+2
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "g1nation",
|
||||
"version": "2.23.0",
|
||||
"version": "2.27.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "g1nation",
|
||||
"version": "2.23.0",
|
||||
"version": "2.27.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"marked": "^18.0.2"
|
||||
@@ -57,7 +57,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1758,7 +1757,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -2656,7 +2654,6 @@
|
||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
@@ -4181,7 +4178,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "g1nation",
|
||||
"displayName": "G1nation",
|
||||
"description": "100% local AI coding agent for VS Code. Create files, edit code, run commands, and work offline with Ollama or LM Studio.",
|
||||
"version": "2.26.0",
|
||||
"description": "High-performance autonomous local AI coding agent for VS Code. Features vectorized inference, asynchronous task management, and 100% offline processing.",
|
||||
"version": "2.32.0",
|
||||
"publisher": "connectailab",
|
||||
"license": "MIT",
|
||||
"icon": "assets/icon.png",
|
||||
|
||||
+75
-57
@@ -22,6 +22,7 @@ import { SessionManager } from './core/session';
|
||||
import { PlannerAgent, ResearcherAgent, WriterAgent } from './agents/factory';
|
||||
import { AgentWorkflowManager } from './agents/AgentWorkflowManager';
|
||||
import { ErrorTranslator } from './core/errorHandler';
|
||||
import { agentEvents, AgentEventTypes } from './core/events';
|
||||
import {
|
||||
AgentExecutionError,
|
||||
FileSystemError,
|
||||
@@ -148,6 +149,7 @@ export class AgentExecutor {
|
||||
public async approveTransaction() {
|
||||
if (!this.transactionManager.isActive()) return;
|
||||
this.transactionManager.commit();
|
||||
agentEvents.emit(AgentEventTypes.TRANSACTION_COMMITTED);
|
||||
this.statusBarManager.updateStatus(AgentStatus.Success, 'Changes committed.');
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: '\n✅ **작업이 승인되어 반영되었습니다.**' });
|
||||
}
|
||||
@@ -155,6 +157,7 @@ export class AgentExecutor {
|
||||
public async rejectTransaction() {
|
||||
if (!this.transactionManager.isActive()) return;
|
||||
this.transactionManager.rollback();
|
||||
agentEvents.emit(AgentEventTypes.TRANSACTION_ROLLED_BACK);
|
||||
this.statusBarManager.updateStatus(AgentStatus.Idle, 'Changes rolled back.');
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: '\n❌ **작업이 거부되어 모든 변경사항이 취소되었습니다.**' });
|
||||
}
|
||||
@@ -399,7 +402,7 @@ export class AgentExecutor {
|
||||
|
||||
if (report.length === 0 && loopDepth === 0 && this.isUnproductiveWaitingReply(aiResponseText)) {
|
||||
assistantMessage.internal = false;
|
||||
const correctedReply = this.buildUnproductiveReplyCorrection(prompt || '');
|
||||
const correctedReply = await this.buildUnproductiveReplyCorrection(prompt || '');
|
||||
assistantMessage.content = correctedReply;
|
||||
this.emitHistoryChanged();
|
||||
this.webview.postMessage({ type: 'streamChunk', value: correctedReply });
|
||||
@@ -459,9 +462,9 @@ export class AgentExecutor {
|
||||
const normalized = prompt.trim().toLowerCase();
|
||||
if (!normalized) return null;
|
||||
|
||||
const projectPath = this.resolveProjectReference(prompt);
|
||||
const projectPath = await this.resolveProjectReference(prompt);
|
||||
if (projectPath && this.isProjectAnalysisRequest(normalized)) {
|
||||
return this.buildProjectAnalysisReply(projectPath);
|
||||
return await this.buildProjectAnalysisReply(projectPath);
|
||||
}
|
||||
|
||||
if (this.isBrainOverviewRequest(normalized)) {
|
||||
@@ -484,12 +487,12 @@ export class AgentExecutor {
|
||||
return null;
|
||||
}
|
||||
|
||||
private resolveProjectReference(prompt: string): string | null {
|
||||
private async resolveProjectReference(prompt: string): Promise<string | null> {
|
||||
const explicitPath = this.extractExistingProjectPath(prompt);
|
||||
if (explicitPath) return explicitPath;
|
||||
|
||||
const namedProject = prompt.match(/([A-Za-z0-9._-]+)\s*(?:프로젝트|project)/i)?.[1];
|
||||
if (!namedProject) return null; // No project keyword found, do not attempt to guess.
|
||||
if (!namedProject) return null;
|
||||
|
||||
const searchRoots = [
|
||||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '',
|
||||
@@ -497,35 +500,38 @@ export class AgentExecutor {
|
||||
].filter(Boolean);
|
||||
|
||||
for (const root of searchRoots) {
|
||||
const resolved = this.findDirectoryByName(root, namedProject, 2); // Depth reduced to 2 for performance and accuracy.
|
||||
const resolved = await this.findDirectoryByNameAsync(root, namedProject, 2);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private findDirectoryByName(root: string, targetName: string, maxDepth: number): string | null {
|
||||
/**
|
||||
* 비차단(Non-blocking) 방식의 디렉토리 검색 (Step 2 최적화)
|
||||
*/
|
||||
private async findDirectoryByNameAsync(root: string, targetName: string, maxDepth: number): Promise<string | null> {
|
||||
if (!root || maxDepth < 0 || !fs.existsSync(root)) return null;
|
||||
const normalizedTarget = targetName.toLowerCase();
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(root, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
|
||||
const entries = await fs.promises.readdir(root, { withFileTypes: true });
|
||||
const dirs = entries.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
|
||||
|
||||
const exact = entries.find(entry => entry.name.toLowerCase() === normalizedTarget);
|
||||
const exact = dirs.find(entry => entry.name.toLowerCase() === normalizedTarget);
|
||||
if (exact) return path.join(root, exact.name);
|
||||
|
||||
const partial = entries.find(entry => entry.name.toLowerCase().includes(normalizedTarget));
|
||||
const partial = dirs.find(entry => entry.name.toLowerCase().includes(normalizedTarget));
|
||||
if (partial) return path.join(root, partial.name);
|
||||
|
||||
for (const entry of entries) {
|
||||
const found = this.findDirectoryByName(path.join(root, entry.name), targetName, maxDepth - 1);
|
||||
if (found) return found;
|
||||
}
|
||||
} catch (error: any) {
|
||||
logError('Project name search failed.', { root, targetName, error: error?.message || String(error) });
|
||||
}
|
||||
// 병렬 탐색으로 성능 최적화
|
||||
const searchPromises = dirs.map(dir => this.findDirectoryByNameAsync(path.join(root, dir.name), targetName, maxDepth - 1));
|
||||
const results = await Promise.all(searchPromises);
|
||||
return results.find(res => res !== null) || null;
|
||||
|
||||
} catch (error: any) {
|
||||
logError('Async project search failed.', { root, targetName, error: error?.message });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -559,13 +565,13 @@ export class AgentExecutor {
|
||||
brainContext,
|
||||
signal,
|
||||
(step, msg) => {
|
||||
this.webview.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` });
|
||||
this.webview?.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` });
|
||||
// 각 단계별 시작을 알림
|
||||
this.webview.postMessage({ type: 'streamChunk', value: `\n\n> **[${step}]** ${msg}\n\n` });
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: `\n\n> **[${step}]** ${msg}\n\n` });
|
||||
}
|
||||
);
|
||||
|
||||
if (signal.aborted) return;
|
||||
if (signal.aborted || !this.webview) return;
|
||||
|
||||
this.webview.postMessage({ type: 'streamChunk', value: `\n\n--- \n\n${finalReport}` });
|
||||
this.webview.postMessage({ type: 'streamEnd' });
|
||||
@@ -641,10 +647,10 @@ export class AgentExecutor {
|
||||
return hasProjectKeyword && hasAnalysisIntent;
|
||||
}
|
||||
|
||||
private buildProjectAnalysisReply(projectPath: string): string {
|
||||
const stat = fs.statSync(projectPath);
|
||||
private async buildProjectAnalysisReply(projectPath: string): Promise<string> {
|
||||
const stat = await fs.promises.stat(projectPath);
|
||||
if (!stat.isDirectory()) {
|
||||
const content = fs.readFileSync(projectPath, 'utf-8');
|
||||
const content = await fs.promises.readFile(projectPath, 'utf-8');
|
||||
return [
|
||||
`요청하신 파일을 실제로 읽었습니다: \`${projectPath}\``,
|
||||
'',
|
||||
@@ -659,8 +665,8 @@ export class AgentExecutor {
|
||||
}
|
||||
|
||||
const packagePath = path.join(projectPath, 'package.json');
|
||||
const readmePath = this.findFirstExisting(projectPath, ['README.md', 'readme.md', 'README.MD']);
|
||||
const files = this.collectProjectFiles(projectPath, 600);
|
||||
const readmePath = await this.findFirstExistingAsync(projectPath, ['README.md', 'readme.md', 'README.MD']);
|
||||
const files = await this.collectProjectFilesAsync(projectPath, 600);
|
||||
const sourceFiles = files.filter(file => /\/src\/|\/app\/|\/pages\/|\/components\/|\/lib\//.test(file));
|
||||
const testFiles = files.filter(file => /\.(test|spec)\.[jt]sx?$|\/__tests__\//.test(file));
|
||||
const configFiles = files.filter(file => /(^|\/)(package\.json|tsconfig\.json|vite\.config\.|next\.config\.|tailwind\.config\.|eslint\.config\.|\.eslintrc|dockerfile|docker-compose|README\.md)/i.test(file));
|
||||
@@ -668,21 +674,25 @@ export class AgentExecutor {
|
||||
let pkg: any = null;
|
||||
if (fs.existsSync(packagePath)) {
|
||||
try {
|
||||
pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
||||
const pkgText = await fs.promises.readFile(packagePath, 'utf-8');
|
||||
pkg = JSON.parse(pkgText);
|
||||
} catch (error: any) {
|
||||
logError('Failed to parse package.json during local project analysis.', { projectPath, error: error?.message || String(error) });
|
||||
logError('Failed to parse package.json during async analysis.', { projectPath, error: error?.message });
|
||||
}
|
||||
}
|
||||
|
||||
const readmeText = readmePath ? fs.readFileSync(readmePath, 'utf-8') : '';
|
||||
const topDirs = this.summarizeTopDirectories(projectPath);
|
||||
const readmeText = readmePath ? await fs.promises.readFile(readmePath, 'utf-8') : '';
|
||||
const topDirs = await this.summarizeTopDirectoriesAsync(projectPath);
|
||||
const stack = this.inferStack(pkg, files);
|
||||
const entryPoints = this.inferEntryPoints(pkg, files);
|
||||
const readmeSummary = this.summarizeReadme(readmeText);
|
||||
const reviewFindings = this.buildProjectReviewFindings({ pkg, files, sourceFiles, testFiles, readmeText });
|
||||
|
||||
// Step 3: 데이터 준비 완료 이벤트 발행 (Observer Pattern)
|
||||
agentEvents.emit(AgentEventTypes.DATA_READY, { projectPath, filesCount: files.length });
|
||||
|
||||
return [
|
||||
`제가 실제로 \`${projectPath}\` 폴더를 읽고 1차 분석했습니다.`,
|
||||
`제가 실제로 \`${projectPath}\` 폴더를 읽고 1차 분석했습니다. (Async Optimized)`,
|
||||
'',
|
||||
'### 📋 제품 개요',
|
||||
'| 항목 | 내용 |',
|
||||
@@ -713,7 +723,7 @@ export class AgentExecutor {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
private findFirstExisting(basePath: string, names: string[]): string | null {
|
||||
private async findFirstExistingAsync(basePath: string, names: string[]): Promise<string | null> {
|
||||
for (const name of names) {
|
||||
const candidate = path.join(basePath, name);
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
@@ -721,34 +731,42 @@ export class AgentExecutor {
|
||||
return null;
|
||||
}
|
||||
|
||||
private collectProjectFiles(dir: string, limit: number, baseDir: string = dir): string[] {
|
||||
private async collectProjectFilesAsync(dir: string, limit: number, baseDir: string = dir): Promise<string[]> {
|
||||
if (limit <= 0 || !fs.existsSync(dir)) return [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
.filter(entry => !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const results: string[] = [];
|
||||
try {
|
||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
const filtered = entries
|
||||
.filter(entry => !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const results: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (results.length >= limit) break;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...this.collectProjectFiles(fullPath, limit - results.length, baseDir));
|
||||
} else {
|
||||
results.push(path.relative(baseDir, fullPath));
|
||||
for (const entry of filtered) {
|
||||
if (results.length >= limit) break;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...await this.collectProjectFilesAsync(fullPath, limit - results.length, baseDir));
|
||||
} else {
|
||||
results.push(path.relative(baseDir, fullPath));
|
||||
}
|
||||
}
|
||||
return results.slice(0, limit);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return results.slice(0, limit);
|
||||
}
|
||||
|
||||
private summarizeTopDirectories(projectPath: string): string[] {
|
||||
return fs.readdirSync(projectPath, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name))
|
||||
.slice(0, 12)
|
||||
.map(entry => {
|
||||
const count = this.collectProjectFiles(path.join(projectPath, entry.name), 200, projectPath).length;
|
||||
return `| \`${entry.name}/\` | 약 ${count}개 |`;
|
||||
});
|
||||
private async summarizeTopDirectoriesAsync(projectPath: string): Promise<string[]> {
|
||||
const entries = await fs.promises.readdir(projectPath, { withFileTypes: true });
|
||||
const dirs = entries.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
|
||||
|
||||
const topDirs = dirs.slice(0, 12);
|
||||
const results = await Promise.all(topDirs.map(async entry => {
|
||||
const files = await this.collectProjectFilesAsync(path.join(projectPath, entry.name), 200, projectPath);
|
||||
return `| \`${entry.name}/\` | 약 ${files.length}개 |`;
|
||||
}));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private inferStack(pkg: any, files: string[]): string[] {
|
||||
@@ -831,10 +849,10 @@ export class AgentExecutor {
|
||||
&& !/<(list_files|read_file|list_brain|read_brain|run_command|edit_file|create_file)/i.test(reply);
|
||||
}
|
||||
|
||||
private buildUnproductiveReplyCorrection(prompt: string): string {
|
||||
const projectPath = this.resolveProjectReference(prompt);
|
||||
private async buildUnproductiveReplyCorrection(prompt: string): Promise<string> {
|
||||
const projectPath = await this.resolveProjectReference(prompt);
|
||||
if (projectPath && this.isProjectAnalysisRequest(prompt.toLowerCase())) {
|
||||
return this.buildProjectAnalysisReply(projectPath);
|
||||
return await this.buildProjectAnalysisReply(projectPath);
|
||||
}
|
||||
|
||||
return '방금 답변은 잘못된 응답입니다. 사용자의 말은 “다음 지시를 달라”가 아니라 지금 바로 처리해야 하는 작업 지시입니다. 제가 먼저 관련 자료를 확인하고, 확인한 내용 기준으로 답변하겠습니다. 가능하면 프로젝트명 대신 정확한 폴더 경로를 함께 주시면 더 안정적으로 분석할 수 있습니다.';
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { PlannerAgent, ResearcherAgent, WriterAgent } from './factory';
|
||||
|
||||
/**
|
||||
* 에이전트 간의 데이터 인계 계약(Contract) 정의
|
||||
*/
|
||||
export interface AgentResult {
|
||||
step: string;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
success: boolean;
|
||||
}
|
||||
import { AgentEngine, PipelineStage } from '../lib/engine';
|
||||
|
||||
export class AgentWorkflowManager {
|
||||
/**
|
||||
* 멀티 에이전트 워크플로우를 강력한 동기화(Synchronization) 하에 실행합니다.
|
||||
* 리팩토링된 고성능 에이전트 엔진을 통해 워크플로우를 실행합니다.
|
||||
*/
|
||||
public static async runStrictWorkflow(
|
||||
prompt: string,
|
||||
@@ -23,35 +14,30 @@ export class AgentWorkflowManager {
|
||||
onProgress: (step: string, message: string) => void
|
||||
): Promise<string> {
|
||||
|
||||
// 1. 에이전트 인스턴스화
|
||||
// 1. 에이전트 준비 (DI를 위한 인스턴스화)
|
||||
const planner = new PlannerAgent(modelName);
|
||||
const researcher = new ResearcherAgent(modelName);
|
||||
const writer = new WriterAgent(modelName);
|
||||
|
||||
// 2. 엔진 인스턴스 생성 (의존성 주입)
|
||||
const engine = new AgentEngine(planner, researcher, writer);
|
||||
|
||||
// 3. 고유 미션 ID 생성 (현재는 타임스탬프 기반)
|
||||
const missionId = `mission_${Date.now()}`;
|
||||
|
||||
try {
|
||||
// --- Phase 1: Planner (Decomposition & Strategy) ---
|
||||
if (signal.aborted) throw new Error('AbortError');
|
||||
onProgress('Planner', '전략 분석 및 작업 분해 중...');
|
||||
const plan = await planner.execute(prompt, brainContext, signal);
|
||||
this.validateResult(plan, 'Planner');
|
||||
|
||||
// --- Phase 2: Researcher (Fact Harvesting) ---
|
||||
if (signal.aborted) throw new Error('AbortError');
|
||||
// 동기화를 위한 의도적 미세 지연 (서버 부하 분산)
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
onProgress('Researcher', '데이터 수집 및 핵심 정보 추출 중...');
|
||||
const research = await researcher.execute(plan, brainContext, signal);
|
||||
this.validateResult(research, 'Researcher');
|
||||
|
||||
// --- Phase 3: Writer (Final Synthesis) ---
|
||||
if (signal.aborted) throw new Error('AbortError');
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
onProgress('Writer', '수집된 정보를 바탕으로 최종 리포트 작성 중...');
|
||||
const finalReport = await writer.execute(research, prompt, signal);
|
||||
this.validateResult(finalReport, 'Writer');
|
||||
|
||||
return finalReport;
|
||||
|
||||
// 4. 엔진을 통한 미션 실행 (Producer-Consumer & Mutex 적용)
|
||||
return await engine.runMission(
|
||||
missionId,
|
||||
prompt,
|
||||
brainContext,
|
||||
signal,
|
||||
(stage: PipelineStage, message: string) => {
|
||||
// UI 피드백을 위한 프로그레스 업데이트
|
||||
const uiStepName = this.mapStageToUI(stage);
|
||||
onProgress(uiStepName, message);
|
||||
}
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError' || error.message.includes('cancelled')) {
|
||||
throw error;
|
||||
@@ -61,12 +47,17 @@ export class AgentWorkflowManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 정합성(Data Integrity) 검증
|
||||
* 엔진 스테이지를 UI 표시용 명칭으로 매핑
|
||||
*/
|
||||
private static validateResult(data: string, step: string) {
|
||||
if (!data || data.trim().length < 20) {
|
||||
const preview = data ? `(Content: "${data.substring(0, 100)}...")` : '(Empty Response)';
|
||||
throw new Error(`${step} 단계에서 생성된 데이터가 불충분합니다. ${preview} 모델을 더 똑똑한 것으로 변경해 보세요.`);
|
||||
}
|
||||
private static mapStageToUI(stage: PipelineStage): string {
|
||||
const maps: Record<PipelineStage, string> = {
|
||||
idle: '대기',
|
||||
planner: 'Planner',
|
||||
researcher: 'Researcher',
|
||||
writer: 'Writer',
|
||||
completed: '완료',
|
||||
error: '오류'
|
||||
};
|
||||
return maps[stage] || '진행 중';
|
||||
}
|
||||
}
|
||||
|
||||
+40
-106
@@ -1,19 +1,14 @@
|
||||
import * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
// axios removed
|
||||
import {
|
||||
_getBrainDir,
|
||||
_isBrainDirExplicitlySet,
|
||||
findBrainFiles,
|
||||
buildApiUrl,
|
||||
logError,
|
||||
logInfo,
|
||||
logWarn,
|
||||
resolveEngine,
|
||||
summarizeText
|
||||
} from './utils';
|
||||
import { getConfig } from './config';
|
||||
import { IAIService, IBrainService, AIService, BrainService } from './core/services';
|
||||
|
||||
export interface BridgeInterface {
|
||||
injectSystemMessage(msg: string): void;
|
||||
@@ -23,10 +18,25 @@ export interface BridgeInterface {
|
||||
findBrainFiles(dir: string): string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* BridgeServer:
|
||||
* 외부 툴(EZER, A.U 등)과 G1nation 확장을 연결하는 통신 브릿지.
|
||||
* 서비스 레이어(AIService, BrainService)를 통해 비즈니스 로직을 분리하여 유지보수성을 극대화했습니다.
|
||||
*/
|
||||
export class BridgeServer {
|
||||
private server: http.Server | null = null;
|
||||
private aiService: IAIService;
|
||||
private brainService: IBrainService;
|
||||
|
||||
constructor(private provider: BridgeInterface) {}
|
||||
constructor(
|
||||
private provider: BridgeInterface,
|
||||
aiService?: IAIService,
|
||||
brainService?: IBrainService
|
||||
) {
|
||||
// 의존성 주입 (DIP): 기본값 제공 및 외부 주입 허용
|
||||
this.aiService = aiService || new AIService();
|
||||
this.brainService = brainService || new BrainService();
|
||||
}
|
||||
|
||||
public start(port: number = 4825) {
|
||||
this.server = http.createServer((req, res) => {
|
||||
@@ -41,16 +51,18 @@ export class BridgeServer {
|
||||
}
|
||||
|
||||
const url = req.url || '';
|
||||
const method = req.method;
|
||||
|
||||
if (req.method === 'GET' && url === '/ping') {
|
||||
// 라우팅 로직 (SRP에 따라 비즈니스 로직은 서비스로 위임)
|
||||
if (method === 'GET' && url === '/ping') {
|
||||
this.handlePing(res);
|
||||
} else if (req.method === 'POST' && url === '/api/exam') {
|
||||
} else if (method === 'POST' && url === '/api/exam') {
|
||||
this.handlePost(req, res, this.processExam.bind(this));
|
||||
} else if (req.method === 'POST' && url === '/api/evaluate') {
|
||||
} else if (method === 'POST' && url === '/api/evaluate') {
|
||||
this.handlePost(req, res, this.processEvaluate.bind(this));
|
||||
} else if (req.method === 'GET' && url === '/api/evaluate-history') {
|
||||
} else if (method === 'GET' && url === '/api/evaluate-history') {
|
||||
this.processEvaluateHistory(res);
|
||||
} else if (req.method === 'POST' && url === '/api/brain-inject') {
|
||||
} else if (method === 'POST' && url === '/api/brain-inject') {
|
||||
this.handlePost(req, res, this.processBrainInject.bind(this));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
@@ -60,9 +72,9 @@ export class BridgeServer {
|
||||
|
||||
this.server.on('error', (err: any) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
logWarn(`Bridge server: Port ${port} is already in use. Another instance might be running.`);
|
||||
logError(`🚫 Bridge Port ${port} in use. Connection with EZER/A.U might fail.`);
|
||||
} else {
|
||||
logError('Bridge server error:', err);
|
||||
logError(`Bridge server error:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -77,7 +89,6 @@ export class BridgeServer {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status: 'ok',
|
||||
msg: 'G1nation Bridge Ready',
|
||||
config: getConfig(),
|
||||
brain: { fileCount: brainCount, enabled: this.provider.brainEnabled }
|
||||
}));
|
||||
@@ -91,7 +102,7 @@ export class BridgeServer {
|
||||
const parsed = JSON.parse(body);
|
||||
await processor(parsed, res);
|
||||
} catch (e: any) {
|
||||
logError('Bridge request failed.', { url: req.url, method: req.method, body: summarizeText(body), error: e?.message || String(e) });
|
||||
logError('Bridge request processor failed.', { url: req.url, error: e.message });
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
@@ -99,21 +110,18 @@ export class BridgeServer {
|
||||
}
|
||||
|
||||
private async processExam(data: any, res: http.ServerResponse) {
|
||||
const prompt = data.prompt || 'Automatic Prompt Received';
|
||||
this.provider.sendPromptFromExtension(`[Bridge Input] ${prompt}`);
|
||||
const result = await this.callAI(prompt);
|
||||
const prompt = data.prompt || 'Automatic Prompt';
|
||||
this.provider.sendPromptFromExtension(`[Bridge] ${prompt}`);
|
||||
const result = await this.aiService.call(prompt);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, rawOutput: result }));
|
||||
}
|
||||
|
||||
private async processEvaluate(data: any, res: http.ServerResponse) {
|
||||
const prompt = data.prompt || '';
|
||||
this.provider.injectSystemMessage(`**[A.U Evaluation Started]**\nAnalyzing input: _"${prompt.substring(0, 60)}..."_`);
|
||||
|
||||
const evaluationPrompt = `[EVALUATION REQUEST]\nPlease evaluate the following input and provide a score/reasoning:\n\n${prompt}`;
|
||||
const result = await this.callAI(evaluationPrompt);
|
||||
|
||||
this.provider.injectSystemMessage(`**[Evaluation Complete]**\n${result.substring(0, 300)}...`);
|
||||
this.provider.injectSystemMessage(`**[A.U Evaluation]** Analyzing input...`);
|
||||
const result = await this.aiService.call(`[EVALUATE] ${prompt}`);
|
||||
this.provider.injectSystemMessage(`**[Result]** ${summarizeText(result, 200)}`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ rawOutput: result }));
|
||||
}
|
||||
@@ -122,96 +130,22 @@ export class BridgeServer {
|
||||
const historyText = this.provider.getHistoryText();
|
||||
if (!historyText || historyText.length < 50) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: "Insufficient chat history for evaluation." }));
|
||||
res.end(JSON.stringify({ error: "Insufficient history" }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.provider.injectSystemMessage(`**[History Evaluation]** Analyzing conversation flow...`);
|
||||
const historyPrompt = `Analyze this conversation history and return a JSON score for Math, Logic, Creative, and Code (0-100):\n\n${historyText.slice(-6000)}`;
|
||||
const result = await this.callAI(historyPrompt);
|
||||
|
||||
const result = await this.aiService.call(`Analyze chat history for metrics (JSON):\n${historyText.slice(-6000)}`);
|
||||
const jsonMatch = result.match(/\{[\s\S]*?\}/);
|
||||
if (jsonMatch) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(jsonMatch[0]);
|
||||
} else {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: "Failed to parse evaluation JSON", raw: result }));
|
||||
}
|
||||
res.writeHead(jsonMatch ? 200 : 500, { 'Content-Type': 'application/json' });
|
||||
res.end(jsonMatch ? jsonMatch[0] : JSON.stringify({ error: "Parse failed", raw: result }));
|
||||
}
|
||||
|
||||
private async processBrainInject(data: any, res: http.ServerResponse) {
|
||||
const { title, markdown, prompt } = data;
|
||||
let brainDir = _getBrainDir();
|
||||
|
||||
if (!fs.existsSync(brainDir)) {
|
||||
fs.mkdirSync(brainDir, { recursive: true });
|
||||
}
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const datePath = path.join(brainDir, '00_Raw', today);
|
||||
fs.mkdirSync(datePath, { recursive: true });
|
||||
|
||||
const safeTitle = title.replace(/[^a-zA-Z0-9가-힣]/gi, '_');
|
||||
const filePath = path.join(datePath, `${safeTitle}.md`);
|
||||
fs.writeFileSync(filePath, markdown, 'utf-8');
|
||||
|
||||
this.provider.injectSystemMessage(`**[Brain Inject]** Knowledge captured: ${title}`);
|
||||
|
||||
const result = await this.callAI(prompt || `Analyze this new knowledge: ${title}`);
|
||||
await this.brainService.inject(title, markdown);
|
||||
this.provider.injectSystemMessage(`**[Brain]** Knowledge captured: ${title}`);
|
||||
const result = await this.aiService.call(prompt || `Analyze: ${title}`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, rawOutput: result }));
|
||||
}
|
||||
|
||||
private async callAI(prompt: string): Promise<string> {
|
||||
const config = getConfig();
|
||||
const primaryEngine = resolveEngine(config.ollamaUrl);
|
||||
const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (const engine of engines) {
|
||||
const apiUrl = buildApiUrl(config.ollamaUrl, engine, 'chat');
|
||||
const payload = engine === 'lmstudio'
|
||||
? {
|
||||
model: config.defaultModel,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
stream: false
|
||||
}
|
||||
: {
|
||||
model: config.defaultModel,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
stream: false
|
||||
};
|
||||
|
||||
try {
|
||||
logInfo('Bridge AI request started.', { engine, apiUrl, model: config.defaultModel });
|
||||
const res = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(config.timeout)
|
||||
});
|
||||
|
||||
const rawText = await res.text();
|
||||
if (!res.ok) {
|
||||
lastError = new Error(`Bridge AI call failed: ${res.status} ${summarizeText(rawText, 250)}`);
|
||||
logError('Bridge AI request returned non-OK status.', { engine, apiUrl, status: res.status, body: summarizeText(rawText, 500) });
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = rawText ? JSON.parse(rawText) as any : {};
|
||||
const content = engine === 'lmstudio'
|
||||
? (data.choices?.[0]?.message?.content || '')
|
||||
: (data.message?.content || data.response || '');
|
||||
|
||||
logInfo('Bridge AI request succeeded.', { engine, apiUrl, responsePreview: summarizeText(content, 200) });
|
||||
return content;
|
||||
} catch (error: any) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
logError('Bridge AI request failed.', { engine, apiUrl, error: lastError.message });
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Bridge AI call failed.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* IDataSource: 데이터 원천에 대한 추상화 인터페이스 (DIP 준수)
|
||||
*/
|
||||
export interface IDataSource<T> {
|
||||
fetch(): Promise<T[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 결과 타입 정의
|
||||
*/
|
||||
export interface AggregateResult {
|
||||
key: string;
|
||||
count: number;
|
||||
values: any[];
|
||||
average?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* DataProcessor:
|
||||
* 시스템의 알고리즘적 효율성과 유지보수성을 극대화하기 위한 핵심 집계 엔진.
|
||||
* O(N) 복잡도를 보장하며 데이터 분포 민감도를 고려한 최적화 전략을 포함합니다.
|
||||
*/
|
||||
export class DataProcessor {
|
||||
/**
|
||||
* 핵심 데이터 집계 함수 (Optimized O(N))
|
||||
* @param data 집계할 데이터 배열
|
||||
* @param keyPath 집계 기준이 될 속성 경로
|
||||
*/
|
||||
public static aggregate(data: any[], keyPath: string): AggregateResult[] {
|
||||
if (!data || data.length === 0) return [];
|
||||
|
||||
// 1. 성능 상충 관계 (Sweet Spot) 고려:
|
||||
// 데이터가 매우 작을 때는(예: N < 10) 해시 맵 생성 오버헤드가 더 클 수 있으나,
|
||||
// 일반적인 성능 보장을 위해 해시 기반 단일 패스(Single-Pass) 방식을 기본으로 채택합니다.
|
||||
|
||||
const map = new Map<string, AggregateResult>();
|
||||
|
||||
for (const item of data) {
|
||||
try {
|
||||
const keyValue = this.getNestedValue(item, keyPath);
|
||||
if (keyValue === undefined || keyValue === null) continue;
|
||||
|
||||
const key = String(keyValue);
|
||||
let entry = map.get(key);
|
||||
|
||||
if (!entry) {
|
||||
entry = {
|
||||
key,
|
||||
count: 0,
|
||||
values: []
|
||||
};
|
||||
map.set(key, entry);
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
entry.values.push(item);
|
||||
|
||||
// 수치형 데이터인 경우 평균 계산을 위한 로직 (예시)
|
||||
if (typeof item.value === 'number') {
|
||||
// 점진적 평균 계산 등 추가 가능
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// 오류 처리 정밀도 (Error Handling Granularity)
|
||||
// 특정 아이템 처리 실패가 전체 집계 중단으로 이어지지 않도록 격리
|
||||
console.warn(`[DataProcessor] Skip item due to error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 분포 민감성(Data Distribution Sensitivity)을 고려한 고도화된 집계 (Trie 기반)
|
||||
* 키가 매우 길거나 계층적인 경우 메모리 및 검색 속도 최적화를 위해 사용합니다.
|
||||
*/
|
||||
public static aggregateByTrie(data: any[], keyPath: string): AggregateResult[] {
|
||||
// TODO: 복잡한 키 구조를 위한 Trie 인덱싱 로직 구현 (Phase 2 확장 예정)
|
||||
return this.aggregate(data, keyPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 중첩된 객체 속성 접근 (Safety handling)
|
||||
*/
|
||||
private static getNestedValue(obj: any, path: string): any {
|
||||
return path.split('.').reduce((prev, curr) => prev && prev[curr], obj);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
/**
|
||||
* AgentEvents: 시스템 전체의 이벤트를 관리하는 관찰자(Observer) 허브.
|
||||
* 모듈 간 결합도를 낮추기 위해 직접 호출 대신 이벤트를 발행-구독합니다.
|
||||
*/
|
||||
export class AgentEvents extends EventEmitter {
|
||||
private static instance: AgentEvents;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
this.setMaxListeners(20);
|
||||
}
|
||||
|
||||
public static getInstance(): AgentEvents {
|
||||
if (!AgentEvents.instance) {
|
||||
AgentEvents.instance = new AgentEvents();
|
||||
}
|
||||
return AgentEvents.instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 표준 이벤트 타입 정의
|
||||
*/
|
||||
export enum AgentEventTypes {
|
||||
DATA_READY = 'data:ready',
|
||||
TASK_STARTED = 'task:started',
|
||||
TASK_COMPLETED = 'task:completed',
|
||||
ERROR_OCCURRED = 'error:occurred',
|
||||
TRANSACTION_COMMITTED = 'transaction:committed',
|
||||
TRANSACTION_ROLLED_BACK = 'transaction:rolled_back'
|
||||
}
|
||||
|
||||
export const agentEvents = AgentEvents.getInstance();
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getConfig } from '../config';
|
||||
import { buildApiUrl, logError, logInfo, resolveEngine, summarizeText, _getBrainDir } from '../utils';
|
||||
|
||||
/**
|
||||
* IAIService: AI 모델 호출에 대한 인터페이스
|
||||
*/
|
||||
export interface IAIService {
|
||||
call(prompt: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* IBrainService: 지식 베이스(Brain) 조작에 대한 인터페이스
|
||||
*/
|
||||
export interface IBrainService {
|
||||
inject(title: string, markdown: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* AIService: Ollama 및 LM Studio 폴백 로직을 포함한 AI 호출 구현체
|
||||
*/
|
||||
export class AIService implements IAIService {
|
||||
public async call(prompt: string): Promise<string> {
|
||||
const config = getConfig();
|
||||
const primaryEngine = resolveEngine(config.ollamaUrl);
|
||||
const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (const engine of engines) {
|
||||
const apiUrl = buildApiUrl(config.ollamaUrl, engine, 'chat');
|
||||
const payload = {
|
||||
model: config.defaultModel,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
stream: false
|
||||
};
|
||||
|
||||
try {
|
||||
logInfo('[AIService] Request started.', { engine, apiUrl });
|
||||
const res = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(config.timeout)
|
||||
});
|
||||
|
||||
const rawText = await res.text();
|
||||
if (!res.ok) {
|
||||
lastError = new Error(`AI call failed: ${res.status} ${summarizeText(rawText, 250)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = rawText ? JSON.parse(rawText) as any : {};
|
||||
const content = engine === 'lmstudio'
|
||||
? (data.choices?.[0]?.message?.content || '')
|
||||
: (data.message?.content || data.response || '');
|
||||
|
||||
return content;
|
||||
} catch (error: any) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
logError(`[AIService] ${engine} failed:`, lastError.message);
|
||||
}
|
||||
}
|
||||
throw lastError || new Error('All AI engines failed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BrainService: 지식 베이스 파일 시스템 저장 및 관리 구현체
|
||||
*/
|
||||
export class BrainService implements IBrainService {
|
||||
public async inject(title: string, markdown: string): Promise<string> {
|
||||
const brainDir = _getBrainDir();
|
||||
if (!fs.existsSync(brainDir)) {
|
||||
fs.mkdirSync(brainDir, { recursive: true });
|
||||
}
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const datePath = path.join(brainDir, '00_Raw', today);
|
||||
if (!fs.existsSync(datePath)) {
|
||||
fs.mkdirSync(datePath, { recursive: true });
|
||||
}
|
||||
|
||||
const safeTitle = title.replace(/[^a-zA-Z0-9가-힣]/gi, '_');
|
||||
const filePath = path.join(datePath, `${safeTitle}.md`);
|
||||
fs.writeFileSync(filePath, markdown, 'utf-8');
|
||||
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { lockManager } from '../core/lock';
|
||||
import { actionQueue } from '../core/queue';
|
||||
import { logInfo, logError } from '../utils';
|
||||
|
||||
/**
|
||||
* 에이전트 인터페이스 정의 (의존성 주입을 위함)
|
||||
*/
|
||||
export interface IAgent {
|
||||
execute(input: string, context?: string, signal?: AbortSignal): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파이프라인 단계 상태 정의
|
||||
*/
|
||||
export type PipelineStage = 'idle' | 'planner' | 'researcher' | 'writer' | 'completed' | 'error';
|
||||
|
||||
/**
|
||||
* AgentEngine:
|
||||
* Producer-Consumer 패턴을 기반으로 멀티 에이전트 워크플로우를 오케스트레이션하는 핵심 엔진.
|
||||
* 명시적 락(Mutex)과 의존성 주입(DI)을 통해 안정성과 유연성을 확보합니다.
|
||||
*/
|
||||
export class AgentEngine {
|
||||
private stage: PipelineStage = 'idle';
|
||||
|
||||
constructor(
|
||||
private readonly planner: IAgent,
|
||||
private readonly researcher: IAgent,
|
||||
private readonly writer: IAgent
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 멀티 에이전트 워크플로우 실행
|
||||
* @param missionId 작업을 식별하기 위한 고유 ID (Mutex 락에 사용)
|
||||
*/
|
||||
public async runMission(
|
||||
missionId: string,
|
||||
prompt: string,
|
||||
brainContext: string,
|
||||
signal: AbortSignal,
|
||||
onProgress: (stage: PipelineStage, message: string) => void
|
||||
): Promise<string> {
|
||||
|
||||
// 1. 명시적 락 획득 (Mutex) - 동일 미션의 중복 실행 방지
|
||||
const release = await lockManager.acquire(`mission_${missionId}`);
|
||||
|
||||
try {
|
||||
// 2. 작업을 비동기 큐에 등록 (Producer-Consumer)
|
||||
return await actionQueue.enqueue(async () => {
|
||||
logInfo(`[AgentEngine] 미션 시작: ${missionId}`);
|
||||
|
||||
// --- Phase 1: Planner ---
|
||||
this.updateStage('planner', '전략 수립 중...', onProgress);
|
||||
if (signal.aborted) throw new Error('AbortError');
|
||||
const plan = await this.planner.execute(prompt, brainContext, signal);
|
||||
this.validateResult(plan, 'Planner');
|
||||
|
||||
// --- Phase 2: Researcher ---
|
||||
this.updateStage('researcher', '핵심 정보 수집 및 분석 중...', onProgress);
|
||||
if (signal.aborted) throw new Error('AbortError');
|
||||
await this.delay(500); // 시스템 부하 분산을 위한 미세 지연
|
||||
const research = await this.researcher.execute(plan, brainContext, signal);
|
||||
this.validateResult(research, 'Researcher');
|
||||
|
||||
// --- Phase 3: Writer ---
|
||||
this.updateStage('writer', '최종 리포트 작성 및 편집 중...', onProgress);
|
||||
if (signal.aborted) throw new Error('AbortError');
|
||||
await this.delay(500);
|
||||
const finalReport = await this.writer.execute(research, prompt, signal);
|
||||
this.validateResult(finalReport, 'Writer');
|
||||
|
||||
this.updateStage('completed', '미션 완료', onProgress);
|
||||
return finalReport;
|
||||
});
|
||||
} catch (error: any) {
|
||||
this.updateStage('error', `오류 발생: ${error.message}`, onProgress);
|
||||
logError(`[AgentEngine] 미션 실패 (${missionId}):`, error);
|
||||
throw error;
|
||||
} finally {
|
||||
// 3. 락 해제
|
||||
release();
|
||||
this.stage = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
private updateStage(stage: PipelineStage, message: string, onProgress: (stage: PipelineStage, message: string) => void) {
|
||||
this.stage = stage;
|
||||
onProgress(stage, message);
|
||||
}
|
||||
|
||||
private validateResult(data: string, step: string) {
|
||||
if (!data || data.trim().length < 10) {
|
||||
throw new Error(`${step} 에이전트로부터 유효한 응답을 받지 못했습니다.`);
|
||||
}
|
||||
}
|
||||
|
||||
private delay(ms: number) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
}
|
||||
@@ -971,7 +971,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
|
||||
.header-controls { display: flex; gap: 8px; margin-left: auto; }
|
||||
|
||||
#promptInput::placeholder { color: var(--accent); opacity: 0.6; font-weight: 500; }
|
||||
#input::placeholder { color: var(--accent); opacity: 0.6; font-weight: 500; }
|
||||
|
||||
|
||||
.msg-body {
|
||||
@@ -1863,7 +1863,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
document.getElementById('historyBtn').addEventListener('click', () => historyOverlay.classList.add('visible'));
|
||||
document.getElementById('closeHistoryBtn').onclick = () => historyOverlay.classList.remove('visible');
|
||||
const updateInputPlaceholder = () => {
|
||||
promptInput.placeholder = \`Ask \${modelSel.value}...\`;
|
||||
if (typeof input !== 'undefined' && input) {
|
||||
input.placeholder = \`Ask \${modelSel ? modelSel.value : 'AI'}...\`;
|
||||
}
|
||||
};
|
||||
|
||||
modelSel.onchange = () => {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/// <reference types="jest" />
|
||||
import { DataProcessor, AggregateResult } from '../src/core/dataProcessor';
|
||||
|
||||
describe('DataProcessor Algorithm & Performance Validation', () => {
|
||||
|
||||
// 1. 정합성 테스트 (Correctness)
|
||||
test('Should correctly aggregate data by key path', () => {
|
||||
const testData = [
|
||||
{ category: 'A', value: 10 },
|
||||
{ category: 'B', value: 20 },
|
||||
{ category: 'A', value: 30 },
|
||||
{ category: 'C', value: 40 },
|
||||
];
|
||||
|
||||
const result = DataProcessor.aggregate(testData, 'category');
|
||||
|
||||
expect(result.length).toBe(3);
|
||||
expect(result.find((r: AggregateResult) => r.key === 'A')?.count).toBe(2);
|
||||
expect(result.find((r: AggregateResult) => r.key === 'B')?.count).toBe(1);
|
||||
});
|
||||
|
||||
// 2. 예외 처리 테스트 (Robustness)
|
||||
test('Should handle invalid or missing key paths gracefully', () => {
|
||||
const testData = [
|
||||
{ id: 1, info: { type: 'X' } },
|
||||
{ id: 2 }, // info.type 없음
|
||||
{ id: 3, info: null }, // info.type 접근 불가
|
||||
];
|
||||
|
||||
const result = DataProcessor.aggregate(testData, 'info.type');
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].key).toBe('X');
|
||||
});
|
||||
|
||||
// 3. 성능 벤치마크 (O(N) vs O(N^2) 검증)
|
||||
test('O(N) Efficiency Benchmark', () => {
|
||||
const generateData = (n: number) => {
|
||||
return Array.from({ length: n }, (_, i) => ({
|
||||
id: i,
|
||||
group: `group_${i % 100}`
|
||||
}));
|
||||
};
|
||||
|
||||
const smallN = 1000;
|
||||
const largeN = 100000; // 100배 증가
|
||||
|
||||
// Small dataset test
|
||||
const smallData = generateData(smallN);
|
||||
const startSmall = performance.now();
|
||||
DataProcessor.aggregate(smallData, 'group');
|
||||
const endSmall = performance.now();
|
||||
const durationSmall = endSmall - startSmall;
|
||||
|
||||
// Large dataset test
|
||||
const largeData = generateData(largeN);
|
||||
const startLarge = performance.now();
|
||||
DataProcessor.aggregate(largeData, 'group');
|
||||
const endLarge = performance.now();
|
||||
const durationLarge = endLarge - startLarge;
|
||||
|
||||
console.log(`[Benchmark] N=${smallN}: ${durationSmall.toFixed(4)}ms`);
|
||||
console.log(`[Benchmark] N=${largeN}: ${durationLarge.toFixed(4)}ms`);
|
||||
console.log(`[Benchmark] Scale Factor (N x 100): ${(durationLarge / durationSmall).toFixed(2)}x time`);
|
||||
|
||||
// O(N^2)이었다면 10,000배 이상의 시간이 걸려야 하지만,
|
||||
// O(N)인 경우 약 100배 내외의 증가폭을 보여야 합니다.
|
||||
expect(durationLarge / durationSmall).toBeLessThan(500);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user