FEAT(app): add MCP tools for server access
- Add MCP tools for real server interaction - Enable K8s and database access
This commit is contained in:
@@ -15,5 +15,5 @@ commonLabels:
|
|||||||
# 이미지 태그 설정 (ArgoCD Image Updater가 자동으로 업데이트)
|
# 이미지 태그 설정 (ArgoCD Image Updater가 자동으로 업데이트)
|
||||||
images:
|
images:
|
||||||
- name: gitea0213.kro.kr/bluemayne/mas
|
- name: gitea0213.kro.kr/bluemayne/mas
|
||||||
newTag: main-sha-1da96753b30d5b0483b7f1a6e7be7944e2b7470b
|
newTag: main-sha-28c6e6f8b827900a25c03f3e63c874ddedecf535
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
MAS (Multi-Agent System) 에이전트 정의
|
MAS (Multi-Agent System) 에이전트 정의
|
||||||
"""
|
"""
|
||||||
from typing import Annotated, Literal, TypedDict
|
from typing import Annotated, Literal, TypedDict, Optional
|
||||||
from langchain_anthropic import ChatAnthropic
|
from langchain_anthropic import ChatAnthropic
|
||||||
from langchain_openai import ChatOpenAI
|
from langchain_openai import ChatOpenAI
|
||||||
from langgraph.graph import StateGraph, END
|
from langgraph.graph import StateGraph, END
|
||||||
from langgraph.prebuilt import ToolNode
|
from langgraph.prebuilt import ToolNode
|
||||||
from langchain_core.messages import HumanMessage, SystemMessage
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
|
from langchain_core.tools import tool
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class AgentState(TypedDict):
|
class AgentState(TypedDict):
|
||||||
@@ -18,12 +23,399 @@ class AgentState(TypedDict):
|
|||||||
result: dict
|
result: dict
|
||||||
|
|
||||||
|
|
||||||
|
# ===== MCP Tools =====
|
||||||
|
|
||||||
|
# === 1. Kubernetes MCP Tools ===
|
||||||
|
@tool
|
||||||
|
def k8s_get_nodes() -> str:
|
||||||
|
"""
|
||||||
|
Get Kubernetes cluster nodes information including status, roles, CPU and memory.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["kubectl", "get", "nodes", "-o", "json"],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
nodes = json.loads(result.stdout)
|
||||||
|
info = []
|
||||||
|
for node in nodes.get("items", []):
|
||||||
|
name = node["metadata"]["name"]
|
||||||
|
labels = node["metadata"].get("labels", {})
|
||||||
|
roles = [k.split("/")[1] for k in labels if "node-role.kubernetes.io" in k]
|
||||||
|
role_str = ",".join(roles) if roles else "worker"
|
||||||
|
|
||||||
|
status = "Unknown"
|
||||||
|
for cond in node["status"]["conditions"]:
|
||||||
|
if cond["type"] == "Ready":
|
||||||
|
status = "Ready" if cond["status"] == "True" else "NotReady"
|
||||||
|
|
||||||
|
capacity = node["status"]["capacity"]
|
||||||
|
cpu = capacity.get("cpu", "?")
|
||||||
|
mem = capacity.get("memory", "?")
|
||||||
|
|
||||||
|
info.append(f"• {name} [{role_str}]: {status} | CPU: {cpu}, Memory: {mem}")
|
||||||
|
|
||||||
|
return f"📦 Kubernetes Nodes ({len(info)}):\n" + "\n".join(info)
|
||||||
|
return f"❌ Error: {result.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ kubectl error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def k8s_get_pods(namespace: str = "", label_selector: str = "") -> str:
|
||||||
|
"""
|
||||||
|
Get Kubernetes pods with optional namespace and label filtering.
|
||||||
|
Args:
|
||||||
|
namespace: Filter by namespace (empty = all namespaces)
|
||||||
|
label_selector: Filter by labels (e.g., "app=myapp")
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = ["kubectl", "get", "pods", "-o", "json"]
|
||||||
|
if namespace:
|
||||||
|
cmd.extend(["-n", namespace])
|
||||||
|
else:
|
||||||
|
cmd.append("--all-namespaces")
|
||||||
|
if label_selector:
|
||||||
|
cmd.extend(["-l", label_selector])
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
if result.returncode == 0:
|
||||||
|
pods = json.loads(result.stdout)
|
||||||
|
info = []
|
||||||
|
for pod in pods.get("items", []):
|
||||||
|
name = pod["metadata"]["name"]
|
||||||
|
ns = pod["metadata"]["namespace"]
|
||||||
|
phase = pod["status"]["phase"]
|
||||||
|
restarts = sum(c.get("restartCount", 0) for c in pod["status"].get("containerStatuses", []))
|
||||||
|
|
||||||
|
emoji = "✅" if phase == "Running" else "⚠️" if phase == "Pending" else "❌"
|
||||||
|
info.append(f"{emoji} {ns}/{name}: {phase} (restarts: {restarts})")
|
||||||
|
|
||||||
|
return f"🐳 Pods ({len(info)}):\n" + "\n".join(info[:50]) # Limit to 50
|
||||||
|
return f"❌ Error: {result.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ kubectl error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def k8s_get_deployments(namespace: str = "") -> str:
|
||||||
|
"""
|
||||||
|
Get Kubernetes deployments with replica status.
|
||||||
|
Args:
|
||||||
|
namespace: Filter by namespace (empty = all namespaces)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = ["kubectl", "get", "deployments", "-o", "json"]
|
||||||
|
if namespace:
|
||||||
|
cmd.extend(["-n", namespace])
|
||||||
|
else:
|
||||||
|
cmd.append("--all-namespaces")
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
if result.returncode == 0:
|
||||||
|
deployments = json.loads(result.stdout)
|
||||||
|
info = []
|
||||||
|
for deploy in deployments.get("items", []):
|
||||||
|
name = deploy["metadata"]["name"]
|
||||||
|
ns = deploy["metadata"]["namespace"]
|
||||||
|
desired = deploy["spec"].get("replicas", 0)
|
||||||
|
ready = deploy["status"].get("readyReplicas", 0)
|
||||||
|
|
||||||
|
emoji = "✅" if ready == desired else "⚠️"
|
||||||
|
info.append(f"{emoji} {ns}/{name}: {ready}/{desired} ready")
|
||||||
|
|
||||||
|
return f"📦 Deployments ({len(info)}):\n" + "\n".join(info[:30])
|
||||||
|
return f"❌ Error: {result.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ kubectl error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def k8s_get_pod_logs(namespace: str, pod_name: str, tail: int = 50) -> str:
|
||||||
|
"""
|
||||||
|
Get logs from a Kubernetes pod.
|
||||||
|
Args:
|
||||||
|
namespace: Pod namespace
|
||||||
|
pod_name: Pod name
|
||||||
|
tail: Number of lines to show (default: 50)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["kubectl", "logs", "-n", namespace, pod_name, f"--tail={tail}"],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return f"📜 Logs for {namespace}/{pod_name}:\n```\n{result.stdout}\n```"
|
||||||
|
return f"❌ Error: {result.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ kubectl error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def k8s_describe_resource(resource_type: str, name: str, namespace: str = "default") -> str:
|
||||||
|
"""
|
||||||
|
Describe a Kubernetes resource (pod, deployment, service, etc.).
|
||||||
|
Args:
|
||||||
|
resource_type: Type of resource (pod, deployment, service, etc.)
|
||||||
|
name: Resource name
|
||||||
|
namespace: Namespace (default: default)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["kubectl", "describe", resource_type, name, "-n", namespace],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Return last 50 lines to keep it manageable
|
||||||
|
lines = result.stdout.split("\n")
|
||||||
|
return f"🔍 Describe {resource_type}/{name} in {namespace}:\n```\n" + "\n".join(lines[-50:]) + "\n```"
|
||||||
|
return f"❌ Error: {result.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ kubectl error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
# === 2. PostgreSQL MCP Tools ===
|
||||||
|
@tool
|
||||||
|
def postgres_query(query: str, database: str = "postgres") -> str:
|
||||||
|
"""
|
||||||
|
Execute a read-only PostgreSQL query.
|
||||||
|
Args:
|
||||||
|
query: SQL query to execute (SELECT only for safety)
|
||||||
|
database: Database name (default: postgres)
|
||||||
|
"""
|
||||||
|
if not query.strip().upper().startswith("SELECT"):
|
||||||
|
return "❌ Only SELECT queries are allowed for safety"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use kubectl exec to run psql in the PostgreSQL pod
|
||||||
|
pg_password = os.getenv("POSTGRES_PASSWORD", "")
|
||||||
|
cmd = [
|
||||||
|
"kubectl", "exec", "-n", "postgresql", "postgresql-primary-0", "--",
|
||||||
|
"env", f"PGPASSWORD={pg_password}",
|
||||||
|
"psql", "-U", "bluemayne", "-d", database, "-c", query
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return f"📊 Query Result:\n```\n{result.stdout}\n```"
|
||||||
|
return f"❌ Error: {result.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ PostgreSQL error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def postgres_list_databases() -> str:
|
||||||
|
"""
|
||||||
|
List all databases in PostgreSQL cluster.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pg_password = os.getenv("POSTGRES_PASSWORD", "")
|
||||||
|
cmd = [
|
||||||
|
"kubectl", "exec", "-n", "postgresql", "postgresql-primary-0", "--",
|
||||||
|
"env", f"PGPASSWORD={pg_password}",
|
||||||
|
"psql", "-U", "bluemayne", "-c", "\\l"
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return f"🗄️ Databases:\n```\n{result.stdout}\n```"
|
||||||
|
return f"❌ Error: {result.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ PostgreSQL error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def postgres_table_info(database: str, table: str) -> str:
|
||||||
|
"""
|
||||||
|
Get table schema information.
|
||||||
|
Args:
|
||||||
|
database: Database name
|
||||||
|
table: Table name
|
||||||
|
"""
|
||||||
|
query = f"SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = '{table}' ORDER BY ordinal_position;"
|
||||||
|
return postgres_query(query, database)
|
||||||
|
|
||||||
|
|
||||||
|
# === 3. Git MCP Tools ===
|
||||||
|
@tool
|
||||||
|
def git_list_repos() -> str:
|
||||||
|
"""
|
||||||
|
List Git repositories in Gitea.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
gitea_url = "https://gitea0213.kro.kr"
|
||||||
|
# This would need Gitea API token - for now, just list known repos
|
||||||
|
known_repos = ["mas", "jaejadle", "jovies", "portfolio", "todo", "cluster-infrastructure"]
|
||||||
|
return f"📚 Git Repositories:\n" + "\n".join(f"• {gitea_url}/bluemayne/{repo}" for repo in known_repos)
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Git error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def git_recent_commits(repo: str, limit: int = 10) -> str:
|
||||||
|
"""
|
||||||
|
Get recent commits from a repository (requires local clone).
|
||||||
|
Args:
|
||||||
|
repo: Repository name
|
||||||
|
limit: Number of commits to show (default: 10)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
repo_path = f"/Users/bluemayne/Projects/{repo}"
|
||||||
|
if not os.path.exists(repo_path):
|
||||||
|
return f"❌ Repository not found: {repo_path}"
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "-C", repo_path, "log", f"-{limit}", "--oneline"],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return f"📝 Recent commits in {repo}:\n```\n{result.stdout}\n```"
|
||||||
|
return f"❌ Error: {result.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Git error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
# === 4. Prometheus MCP Tools ===
|
||||||
|
@tool
|
||||||
|
def prometheus_query(query: str) -> str:
|
||||||
|
"""
|
||||||
|
Execute a PromQL query against Prometheus.
|
||||||
|
Args:
|
||||||
|
query: PromQL query (e.g., "up", "node_cpu_seconds_total")
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Prometheus is accessible via kubectl port-forward or ingress
|
||||||
|
# For now, use kubectl proxy approach
|
||||||
|
result = subprocess.run(
|
||||||
|
["kubectl", "exec", "-n", "monitoring", "deployment/prometheus-kube-prometheus-operator", "--",
|
||||||
|
"wget", "-qO-", f"http://localhost:9090/api/v1/query?query={query}"],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
if data.get("status") == "success":
|
||||||
|
results = data.get("data", {}).get("result", [])
|
||||||
|
output = []
|
||||||
|
for r in results[:20]: # Limit to 20 results
|
||||||
|
metric = r.get("metric", {})
|
||||||
|
value = r.get("value", [None, "N/A"])[1]
|
||||||
|
output.append(f"• {metric}: {value}")
|
||||||
|
return f"📈 Prometheus Query Result:\n" + "\n".join(output)
|
||||||
|
return f"❌ Query failed: {data}"
|
||||||
|
return f"❌ Error: {result.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Prometheus error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def prometheus_node_metrics() -> str:
|
||||||
|
"""
|
||||||
|
Get node resource usage metrics from Prometheus.
|
||||||
|
"""
|
||||||
|
query = "node_cpu_seconds_total"
|
||||||
|
return prometheus_query(query)
|
||||||
|
|
||||||
|
|
||||||
|
# === 5. File System MCP Tools ===
|
||||||
|
@tool
|
||||||
|
def fs_read_file(file_path: str, max_lines: int = 100) -> str:
|
||||||
|
"""
|
||||||
|
Read a file from the filesystem (with safety checks).
|
||||||
|
Args:
|
||||||
|
file_path: Path to file
|
||||||
|
max_lines: Maximum lines to read (default: 100)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Only allow reading from safe directories
|
||||||
|
safe_dirs = ["/var/log", "/tmp", os.path.expanduser("~/Projects")]
|
||||||
|
if not any(file_path.startswith(d) for d in safe_dirs):
|
||||||
|
return f"❌ Access denied: {file_path} (not in safe directories)"
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return f"❌ File not found: {file_path}"
|
||||||
|
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
lines = f.readlines()[:max_lines]
|
||||||
|
|
||||||
|
return f"📄 File: {file_path}\n```\n{''.join(lines)}\n```"
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ File read error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def fs_list_directory(dir_path: str) -> str:
|
||||||
|
"""
|
||||||
|
List contents of a directory.
|
||||||
|
Args:
|
||||||
|
dir_path: Directory path
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(dir_path):
|
||||||
|
return f"❌ Directory not found: {dir_path}"
|
||||||
|
|
||||||
|
if not os.path.isdir(dir_path):
|
||||||
|
return f"❌ Not a directory: {dir_path}"
|
||||||
|
|
||||||
|
items = os.listdir(dir_path)
|
||||||
|
dirs = [f"📁 {item}/" for item in items if os.path.isdir(os.path.join(dir_path, item))]
|
||||||
|
files = [f"📄 {item}" for item in items if os.path.isfile(os.path.join(dir_path, item))]
|
||||||
|
|
||||||
|
return f"📂 Directory: {dir_path}\n" + "\n".join(sorted(dirs) + sorted(files))
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Directory list error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
# === 6. Docker/Container Registry MCP Tools ===
|
||||||
|
@tool
|
||||||
|
def docker_list_images(registry: str = "gitea0213.kro.kr") -> str:
|
||||||
|
"""
|
||||||
|
List Docker images in registry.
|
||||||
|
Args:
|
||||||
|
registry: Registry URL (default: gitea0213.kro.kr)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# List known images
|
||||||
|
known_images = ["mas", "jaejadle", "jovies", "portfolio", "todo"]
|
||||||
|
return f"🐳 Docker Images in {registry}:\n" + "\n".join(
|
||||||
|
f"• {registry}/bluemayne/{img}:latest" for img in known_images
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Docker error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
# MCP Tools Collection
|
||||||
|
mcp_tools = [
|
||||||
|
# Kubernetes
|
||||||
|
k8s_get_nodes,
|
||||||
|
k8s_get_pods,
|
||||||
|
k8s_get_deployments,
|
||||||
|
k8s_get_pod_logs,
|
||||||
|
k8s_describe_resource,
|
||||||
|
# PostgreSQL
|
||||||
|
postgres_query,
|
||||||
|
postgres_list_databases,
|
||||||
|
postgres_table_info,
|
||||||
|
# Git
|
||||||
|
git_list_repos,
|
||||||
|
git_recent_commits,
|
||||||
|
# Prometheus
|
||||||
|
prometheus_query,
|
||||||
|
prometheus_node_metrics,
|
||||||
|
# File System
|
||||||
|
fs_read_file,
|
||||||
|
fs_list_directory,
|
||||||
|
# Docker
|
||||||
|
docker_list_images,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ===== 1. Claude Code - Orchestrator =====
|
# ===== 1. Claude Code - Orchestrator =====
|
||||||
claude_orchestrator = ChatAnthropic(
|
claude_orchestrator = ChatAnthropic(
|
||||||
model="claude-sonnet-4-5", # Latest Claude Sonnet 4.5 (Sep 2025)
|
model="claude-sonnet-4-5", # Latest Claude Sonnet 4.5 (Sep 2025)
|
||||||
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
||||||
temperature=0
|
temperature=0
|
||||||
)
|
).bind_tools(mcp_tools) # Bind MCP tools to Claude
|
||||||
|
|
||||||
ORCHESTRATOR_PROMPT = """당신은 MAS의 총괄 조율자이자 DevOps 전문가입니다.
|
ORCHESTRATOR_PROMPT = """당신은 MAS의 총괄 조율자이자 DevOps 전문가입니다.
|
||||||
|
|
||||||
@@ -38,12 +430,42 @@ ORCHESTRATOR_PROMPT = """당신은 MAS의 총괄 조율자이자 DevOps 전문
|
|||||||
2. frontend_developer: Next.js, React 프론트엔드 개발
|
2. frontend_developer: Next.js, React 프론트엔드 개발
|
||||||
3. sre_specialist: 모니터링, 성능 최적화, 보안
|
3. sre_specialist: 모니터링, 성능 최적화, 보안
|
||||||
|
|
||||||
**중요한 원칙**:
|
**사용 가능한 도구(Tools)**:
|
||||||
- 실제 클러스터 정보에 접근할 수 없으므로, 구체적인 숫자나 상태는 추측하지 마세요
|
당신은 실제 서버 상태에 접근할 수 있는 다양한 도구를 사용할 수 있습니다:
|
||||||
- 모르는 정보는 "실제 확인이 필요합니다" 또는 "kubectl 명령으로 확인해주세요"라고 명시하세요
|
|
||||||
- 일반적인 모범 사례와 권장사항만 제공하세요
|
|
||||||
|
|
||||||
요청을 분석하고 어떤 에이전트가 처리해야 할지 결정하세요.
|
1. **Kubernetes 도구**:
|
||||||
|
- k8s_get_nodes(): 노드 상태 조회
|
||||||
|
- k8s_get_pods(namespace, label_selector): Pod 목록 조회
|
||||||
|
- k8s_get_deployments(namespace): Deployment 목록
|
||||||
|
- k8s_get_pod_logs(namespace, pod_name, tail): Pod 로그 확인
|
||||||
|
- k8s_describe_resource(resource_type, name, namespace): 리소스 상세 정보
|
||||||
|
|
||||||
|
2. **PostgreSQL 도구**:
|
||||||
|
- postgres_query(query, database): SQL SELECT 쿼리 실행
|
||||||
|
- postgres_list_databases(): 데이터베이스 목록
|
||||||
|
- postgres_table_info(database, table): 테이블 스키마 정보
|
||||||
|
|
||||||
|
3. **Git 도구**:
|
||||||
|
- git_list_repos(): 레포지토리 목록
|
||||||
|
- git_recent_commits(repo, limit): 최근 커밋 조회
|
||||||
|
|
||||||
|
4. **Prometheus 도구**:
|
||||||
|
- prometheus_query(query): PromQL 쿼리 실행
|
||||||
|
- prometheus_node_metrics(): 노드 메트릭 조회
|
||||||
|
|
||||||
|
5. **파일 시스템 도구**:
|
||||||
|
- fs_read_file(file_path, max_lines): 파일 읽기
|
||||||
|
- fs_list_directory(dir_path): 디렉토리 목록
|
||||||
|
|
||||||
|
6. **Docker 도구**:
|
||||||
|
- docker_list_images(registry): 레지스트리 이미지 목록
|
||||||
|
|
||||||
|
**사용 방법**:
|
||||||
|
- 사용자가 클러스터 상태, 로그, 데이터베이스 등을 물어보면 **반드시 도구를 사용**하여 실제 정보를 확인하세요
|
||||||
|
- 추측하지 말고, 도구를 통해 확인한 실제 데이터를 기반으로 답변하세요
|
||||||
|
- 여러 정보가 필요한 경우 여러 도구를 순차적으로 사용하세요
|
||||||
|
|
||||||
|
요청을 분석하고 필요한 도구를 사용한 후, 적절한 에이전트에게 작업을 할당하세요.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -117,28 +539,60 @@ SRE_PROMPT = """당신은 SRE(Site Reliability Engineer) 전문가입니다.
|
|||||||
|
|
||||||
|
|
||||||
def orchestrator_node(state: AgentState) -> AgentState:
|
def orchestrator_node(state: AgentState) -> AgentState:
|
||||||
"""Claude Code - 작업 분석 및 할당"""
|
"""Claude Code - 작업 분석 및 할당 (도구 사용 가능)"""
|
||||||
messages = state["messages"]
|
messages = state["messages"]
|
||||||
|
|
||||||
|
# Claude 호출
|
||||||
response = claude_orchestrator.invoke([
|
response = claude_orchestrator.invoke([
|
||||||
SystemMessage(content=ORCHESTRATOR_PROMPT),
|
SystemMessage(content=ORCHESTRATOR_PROMPT),
|
||||||
HumanMessage(content=messages[-1]["content"])
|
HumanMessage(content=messages[-1]["content"])
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# 도구 호출이 있는 경우 처리
|
||||||
|
tool_outputs = []
|
||||||
|
if hasattr(response, 'tool_calls') and response.tool_calls:
|
||||||
|
for tool_call in response.tool_calls:
|
||||||
|
tool_name = tool_call['name']
|
||||||
|
tool_args = tool_call.get('args', {})
|
||||||
|
|
||||||
|
# 도구 실행
|
||||||
|
try:
|
||||||
|
tool_func = next(t for t in mcp_tools if t.name == tool_name)
|
||||||
|
tool_result = tool_func.invoke(tool_args)
|
||||||
|
tool_outputs.append(f"\n🔧 **{tool_name}**: {tool_result}")
|
||||||
|
except Exception as e:
|
||||||
|
tool_outputs.append(f"\n❌ **{tool_name}** failed: {str(e)}")
|
||||||
|
|
||||||
|
# 도구 결과를 포함하여 다시 Claude 호출
|
||||||
|
if tool_outputs:
|
||||||
|
tool_context = "\n".join(tool_outputs)
|
||||||
|
response = claude_orchestrator.invoke([
|
||||||
|
SystemMessage(content=ORCHESTRATOR_PROMPT),
|
||||||
|
HumanMessage(content=messages[-1]["content"]),
|
||||||
|
HumanMessage(content=f"도구 실행 결과:\n{tool_context}")
|
||||||
|
])
|
||||||
|
|
||||||
|
# 응답 내용 추출
|
||||||
|
content = response.content if isinstance(response.content, str) else str(response.content)
|
||||||
|
|
||||||
|
# 도구 출력 추가
|
||||||
|
if tool_outputs:
|
||||||
|
content = "\n".join(tool_outputs) + "\n\n" + content
|
||||||
|
|
||||||
# 작업 타입 결정
|
# 작업 타입 결정
|
||||||
content = response.content.lower()
|
content_lower = content.lower()
|
||||||
if "backend" in content or "api" in content or "fastapi" in content:
|
if "backend" in content_lower or "api" in content_lower or "fastapi" in content_lower:
|
||||||
next_agent = "backend_developer"
|
next_agent = "backend_developer"
|
||||||
elif "frontend" in content or "ui" in content or "react" in content:
|
elif "frontend" in content_lower or "ui" in content_lower or "react" in content_lower:
|
||||||
next_agent = "frontend_developer"
|
next_agent = "frontend_developer"
|
||||||
elif "monitoring" in content or "performance" in content or "sre" in content:
|
elif "monitoring" in content_lower or "performance" in content_lower or "sre" in content_lower:
|
||||||
next_agent = "sre_specialist"
|
next_agent = "sre_specialist"
|
||||||
else:
|
else:
|
||||||
next_agent = "orchestrator" # 자신이 직접 처리
|
next_agent = "orchestrator" # 자신이 직접 처리
|
||||||
|
|
||||||
state["messages"].append({
|
state["messages"].append({
|
||||||
"role": "orchestrator",
|
"role": "orchestrator",
|
||||||
"content": response.content
|
"content": content
|
||||||
})
|
})
|
||||||
state["current_agent"] = next_agent
|
state["current_agent"] = next_agent
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ psycopg2-binary==2.9.10
|
|||||||
|
|
||||||
# MCP (Model Context Protocol)
|
# MCP (Model Context Protocol)
|
||||||
mcp==1.1.2
|
mcp==1.1.2
|
||||||
|
httpx==0.28.1
|
||||||
|
kubernetes==31.0.0
|
||||||
|
psycopg2-binary==2.9.10
|
||||||
|
GitPython==3.1.43
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user