Files
deepagent/deepagents_sourcecode/libs/deepagents-cli/deepagents_cli/skills/middleware.py
HyunjunJeon 9cb01f4abe project init
2025-12-31 11:32:36 +09:00

274 lines
11 KiB
Python

"""에이전트 기술을 시스템 프롬프트에 로드하고 노출하기 위한 미들웨어.
이 미들웨어는 점진적 노출(progressive disclosure)을 통해 Anthropic의 "Agent Skills" 패턴을 구현합니다:
1. 세션 시작 시 SKILL.md 파일에서 YAML frontmatter 파싱
2. 시스템 프롬프트에 기술 메타데이터(이름 + 설명) 주입
3. 에이전트는 작업과 관련이 있을 때 SKILL.md의 전체 내용을 읽음
기술 디렉토리 구조 (에이전트별 + 프로젝트):
사용자 수준: ~/.deepagents/{AGENT_NAME}/skills/
프로젝트 수준: {PROJECT_ROOT}/.deepagents/skills/
구조 예시:
~/.deepagents/{AGENT_NAME}/skills/
├── web-research/
│ ├── SKILL.md # 필수: YAML frontmatter + 지침
│ └── helper.py # 선택 사항: 지원 파일
├── code-review/
│ ├── SKILL.md
│ └── checklist.md
.deepagents/skills/
├── project-specific/
│ └── SKILL.md # 프로젝트 전용 기술
"""
from collections.abc import Awaitable, Callable
from pathlib import Path
from typing import NotRequired, TypedDict, cast
from langchain.agents.middleware.types import (
AgentMiddleware,
AgentState,
ModelRequest,
ModelResponse,
)
from langgraph.runtime import Runtime
from deepagents_cli.skills.load import SkillMetadata, list_skills
class SkillsState(AgentState):
"""기술 미들웨어를 위한 상태."""
skills_metadata: NotRequired[list[SkillMetadata]]
"""로드된 기술 메타데이터 목록 (이름, 설명, 경로)."""
class SkillsStateUpdate(TypedDict):
"""기술 미들웨어를 위한 상태 업데이트."""
skills_metadata: list[SkillMetadata]
"""로드된 기술 메타데이터 목록 (이름, 설명, 경로)."""
# 기술 시스템 문서
SKILLS_SYSTEM_PROMPT = """
## 기술 시스템 (Skills System)
당신은 전문적인 능력과 도메인 지식을 제공하는 기술 라이브러리에 접근할 수 있습니다.
{skills_locations}
**사용 가능한 기술:**
{skills_list}
**기술 사용 방법 (점진적 노출):**
기술은 **점진적 노출(progressive disclosure)** 패턴을 따릅니다. 당신은 기술이 존재한다는 것(위의 이름 + 설명)은 알고 있지만, 필요할 때만 전체 지침을 읽습니다:
1. **기술이 적용되는 시기 파악**: 사용자의 작업이 기술의 설명과 일치하는지 확인하십시오.
2. **기술의 전체 지침 읽기**: 위의 기술 목록은 read_file과 함께 사용할 정확한 경로를 보여줍니다.
3. **기술의 지침 따르기**: SKILL.md에는 단계별 워크플로우, 권장 사항 및 예시가 포함되어 있습니다.
4. **지원 파일 접근**: 기술에는 Python 스크립트, 설정 또는 참조 문서가 포함될 수 있습니다. 절대 경로를 사용하십시오.
**기술을 사용해야 하는 경우:**
- 사용자의 요청이 기술의 도메인과 일치할 때 (예: "X 조사해줘" → web-research 기술)
- 전문 지식이나 구조화된 워크플로우가 필요할 때
- 기술이 복잡한 작업에 대해 검증된 패턴을 제공할 때
**기술은 자체 문서화됨:**
- 각 SKILL.md는 기술이 수행하는 작업과 사용 방법을 정확하게 알려줍니다.
- 위의 기술 목록은 각 기술의 SKILL.md 파일에 대한 전체 경로를 보여줍니다.
**기술 스크립트 실행:**
기술에는 Python 스크립트나 기타 실행 파일이 포함될 수 있습니다. 항상 기술 목록의 절대 경로를 사용하십시오.
**워크플로우 예시:**
사용자: "양자 컴퓨팅의 최신 개발 동향을 조사해 줄 수 있어?"
1. 위에서 사용 가능한 기술 확인 → 전체 경로와 함께 "web-research" 기술 확인
2. 목록에 표시된 경로를 사용하여 기술 읽기
3. 기술의 조사 워크플로우 따르기 (조사 → 정리 → 합성)
4. 절대 경로와 함께 헬퍼 스크립트 사용
주의: 기술은 당신을 더 유능하고 일관성 있게 만드는 도구입니다. 의심스러울 때는 해당 작업에 대한 기술이 있는지 확인하십시오!
"""
class SkillsMiddleware(AgentMiddleware):
"""에이전트 기술을 로드하고 노출하기 위한 미들웨어.
이 미들웨어는 Anthropic의 에이전트 기술 패턴을 구현합니다:
- 세션 시작 시 YAML frontmatter에서 기술 메타데이터(이름, 설명)를 로드함
- 발견 가능성을 위해 시스템 프롬프트에 기술 목록을 주입함
- 기술이 관련 있을 때 에이전트가 전체 SKILL.md 내용을 읽음 (점진적 노출)
사용자 수준 및 프로젝트 수준 기술을 모두 지원합니다:
- 사용자 기술: ~/.deepagents/{AGENT_NAME}/skills/
- 프로젝트 기술: {PROJECT_ROOT}/.deepagents/skills/
- 프로젝트 기술은 이름이 같은 사용자 기술을 오버라이드함
Args:
skills_dir: 사용자 수준 기술 디렉토리 경로 (에이전트별).
assistant_id: 프롬프트의 경로 참조를 위한 에이전트 식별자.
project_skills_dir: 선택적인 프로젝트 수준 기술 디렉토리 경로.
"""
state_schema = SkillsState
def __init__(
self,
*,
skills_dir: str | Path,
assistant_id: str,
project_skills_dir: str | Path | None = None,
) -> None:
"""기술 미들웨어를 초기화합니다.
Args:
skills_dir: 사용자 수준 기술 디렉토리 경로.
assistant_id: 에이전트 식별자.
project_skills_dir: 선택적인 프로젝트 수준 기술 디렉토리 경로.
"""
self.skills_dir = Path(skills_dir).expanduser()
self.assistant_id = assistant_id
self.project_skills_dir = Path(project_skills_dir).expanduser() if project_skills_dir else None
# 프롬프트 표시를 위한 경로 저장
self.user_skills_display = f"~/.deepagents/{assistant_id}/skills"
self.system_prompt_template = SKILLS_SYSTEM_PROMPT
def _format_skills_locations(self) -> str:
"""시스템 프롬프트 표시를 위해 기술 위치 형식을 지정합니다."""
locations = [f"**사용자 기술**: `{self.user_skills_display}`"]
if self.project_skills_dir:
locations.append(f"**프로젝트 기술**: `{self.project_skills_dir}` (사용자 기술을 오버라이드함)")
return "\n".join(locations)
def _format_skills_list(self, skills: list[SkillMetadata]) -> str:
"""시스템 프롬프트 표시를 위해 기술 메타데이터 형식을 지정합니다."""
if not skills:
locations = [f"{self.user_skills_display}/"]
if self.project_skills_dir:
locations.append(f"{self.project_skills_dir}/")
return f"(현재 사용 가능한 기술이 없습니다. {' 또는 '.join(locations)} 에 기술을 생성할 수 있습니다)"
# 출처별로 기술 그룹화
user_skills = [s for s in skills if s["source"] == "user"]
project_skills = [s for s in skills if s["source"] == "project"]
lines = []
# 사용자 기술 표시
if user_skills:
lines.append("**사용자 기술:**")
for skill in user_skills:
lines.append(f"- **{skill['name']}**: {skill['description']}")
lines.append(f" → 전체 지침을 보려면 `{skill['path']}` 읽기")
lines.append("")
# 프로젝트 기술 표시
if project_skills:
lines.append("**프로젝트 기술:**")
for skill in project_skills:
lines.append(f"- **{skill['name']}**: {skill['description']}")
lines.append(f" → 전체 지침을 보려면 `{skill['path']}` 읽기")
return "\n".join(lines)
def before_agent(self, state: SkillsState, runtime: Runtime) -> SkillsStateUpdate | None:
"""에이전트 실행 전 기술 메타데이터를 로드합니다.
이는 사용자 수준 및 프로젝트 수준 디렉토리 모두에서 사용 가능한 기술을 검색하기 위해
세션 시작 시 한 번 실행됩니다.
Args:
state: 현재 에이전트 상태.
runtime: 런타임 컨텍스트.
Returns:
skills_metadata가 채워진 업데이트된 상태.
"""
# 기술 디렉토리의 변경 사항을 포착하기 위해
# 에이전트와의 매 상호 작용마다 기술을 다시 로드합니다.
skills = list_skills(
user_skills_dir=self.skills_dir,
project_skills_dir=self.project_skills_dir,
)
return SkillsStateUpdate(skills_metadata=skills)
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
"""시스템 프롬프트에 기술 문서를 주입합니다.
이것은 기술 정보가 항상 사용 가능하도록 매 모델 호출 시 실행됩니다.
Args:
request: 처리 중인 모델 요청.
handler: 수정된 요청으로 호출할 핸들러 함수.
Returns:
핸들러의 모델 응답.
"""
# 상태에서 기술 메타데이터 가져오기
skills_metadata = request.state.get("skills_metadata", [])
# 기술 위치 및 목록 형식 지정
skills_locations = self._format_skills_locations()
skills_list = self._format_skills_list(skills_metadata)
# 기술 문서 형식 지정
skills_section = self.system_prompt_template.format(
skills_locations=skills_locations,
skills_list=skills_list,
)
if request.system_prompt:
system_prompt = request.system_prompt + "\n\n" + skills_section
else:
system_prompt = skills_section
return handler(request.override(system_prompt=system_prompt))
async def awrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
) -> ModelResponse:
"""(비동기) 시스템 프롬프트에 기술 문서를 주입합니다.
Args:
request: 처리 중인 모델 요청.
handler: 수정된 요청으로 호출할 핸들러 함수.
Returns:
핸들러의 모델 응답.
"""
# state_schema로 인해 상태는 SkillsState임이 보장됨
state = cast("SkillsState", request.state)
skills_metadata = state.get("skills_metadata", [])
# 기술 위치 및 목록 형식 지정
skills_locations = self._format_skills_locations()
skills_list = self._format_skills_list(skills_metadata)
# 기술 문서 형식 지정
skills_section = self.system_prompt_template.format(
skills_locations=skills_locations,
skills_list=skills_list,
)
# 시스템 프롬프트에 주입
if request.system_prompt:
system_prompt = request.system_prompt + "\n\n" + skills_section
else:
system_prompt = skills_section
return await handler(request.override(system_prompt=system_prompt))