From 346b0c79ef5003d297e3e1a928748dd052c1d8f7 Mon Sep 17 00:00:00 2001 From: Mayne0213 Date: Tue, 23 Dec 2025 22:31:45 +0900 Subject: [PATCH] INIT(app): initial setup - Initialize project structure - Add base application files --- .gitignore | 58 +++++++ README.md | 261 ++++++++++++++++++++++++++++++ deploy/argocd/mas.yaml | 21 +++ deploy/docker/Dockerfile | 24 +++ deploy/docker/docker-compose.yml | 73 +++++++++ deploy/k8s/deployment.yaml | 73 +++++++++ deploy/k8s/ingress.yaml | 29 ++++ deploy/k8s/kustomization.yaml | 13 ++ deploy/k8s/namespace.yaml | 7 + deploy/k8s/service.yaml | 17 ++ deploy/vault/mas-api-keys.yaml | 31 ++++ deploy/vault/mas-postgres.yaml | 27 ++++ services/backend/.chainlit | 17 ++ services/backend/agents.py | 240 +++++++++++++++++++++++++++ services/backend/chainlit_app.py | 85 ++++++++++ services/backend/requirements.txt | 30 ++++ 16 files changed, 1006 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 deploy/argocd/mas.yaml create mode 100644 deploy/docker/Dockerfile create mode 100644 deploy/docker/docker-compose.yml create mode 100644 deploy/k8s/deployment.yaml create mode 100644 deploy/k8s/ingress.yaml create mode 100644 deploy/k8s/kustomization.yaml create mode 100644 deploy/k8s/namespace.yaml create mode 100644 deploy/k8s/service.yaml create mode 100644 deploy/vault/mas-api-keys.yaml create mode 100644 deploy/vault/mas-postgres.yaml create mode 100644 services/backend/.chainlit create mode 100644 services/backend/agents.py create mode 100644 services/backend/chainlit_app.py create mode 100644 services/backend/requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67ab2bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ +.venv +pip-log.txt +pip-delete-this-directory.txt +.pytest_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ + +# Environment variables +.env +.env.local +.env.*.local +*.env + +# Chainlit +.chainlit/ +.files/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Logs +*.log +logs/ + +# Docker +docker-compose.override.yml + +# Kubernetes secrets (local only) +*-secret.yaml +!deploy/vault/*.yaml + +# Node modules (if any frontend added) +node_modules/ +package-lock.json +yarn.lock + +# Temporary files +tmp/ +temp/ +*.tmp + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1469430 --- /dev/null +++ b/README.md @@ -0,0 +1,261 @@ +# MAS (Multi-Agent System) + +MAS is a unified UI and orchestration layer for multiple AI agents (similar to ChatGPT, Claude, Gemini), running on your own Kubernetes cluster. + +## 🎯 Architecture + +### Agents +- **Claude Code (Orchestrator)**: overall coordinator & DevOps expert +- **Qwen Backend**: backend engineer (FastAPI, Node.js) +- **Qwen Frontend**: frontend engineer (Next.js, React) +- **Qwen SRE**: monitoring & reliability engineer + +### Tech stack +- **Backend**: LangGraph + LangChain + FastAPI +- **UI**: Chainlit (chat-style UI) +- **Database**: PostgreSQL (CNPG) +- **Cache**: Redis +- **LLMs**: Claude API + **Groq Llama 3.x** (OpenAI-compatible API) +- **Deploy**: Kubernetes + ArgoCD + +--- + +## πŸš€ Local development + +### 1. Run with Docker Compose + +```bash +cd deploy/docker + +# Copy or create .env and fill in your API keys +# (ANTHROPIC_API_KEY, GROQ_API_KEY, etc.) + +# Start the full stack +docker compose up -d + +# Tail logs +docker compose logs -f mas +``` + +Open: `http://localhost:8000` + +### 2. Run backend directly (Python) + +```bash +cd services/backend + +# Create venv +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt + +# Environment variables +cp .env.example .env +# Edit .env and set your API keys + +# Run Chainlit app +chainlit run chainlit_app.py +``` + +--- + +## ☸️ Kubernetes deployment + +### 1. Create namespace and secrets + +```bash +kubectl create namespace mas + +kubectl create secret generic mas-api-keys \ + --from-literal=anthropic-api-key=YOUR_CLAUDE_KEY \ + --from-literal=openai-api-key=YOUR_OPENAI_KEY \ + --from-literal=google-api-key=YOUR_GEMINI_KEY \ + -n mas +``` + +### 2. Deploy via ArgoCD + +```bash +# Create ArgoCD Application +kubectl apply -f deploy/argocd/mas.yaml + +# Sync and check status +argocd app sync mas +argocd app get mas +``` + +### 3. Deploy from your server (example) + +```bash +# SSH into your k3s master +ssh oracle-master + +# Apply ArgoCD Application +sudo kubectl apply -f /path/to/deploy/argocd/mas.yaml + +# Check status +sudo kubectl get pods -n mas +sudo kubectl logs -f deployment/mas -n mas +``` + +Ingress example (if configured): `https://mas.mayne.vcn` + +--- + +## 🎨 UI customization + +### Chainlit theme & behavior + +You can customize the UI via `services/backend/.chainlit`: + +```toml +[UI] +name = "MAS" +show_readme_as_default = true +default_collapse_content = true +``` + +### Agent prompts + +System prompts for each agent live in `services/backend/agents.py`. +You can tune: +- how the **Orchestrator** routes tasks +- coding style of backend/frontend agents +- SRE troubleshooting behavior + +--- + +## πŸ“Š Observability + +### Prometheus ServiceMonitor (example) + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: mas + namespace: mas +spec: + selector: + matchLabels: + app: mas + endpoints: + - port: http + path: /metrics +``` + +### Grafana dashboards + +Recommended panels: +- LangGraph workflow metrics +- Per-agent latency & error rate +- Token usage and cost estimates +- Backend API latency & 5xx rate + +--- + +## πŸ”§ Advanced features + +### 1. MCP (Model Context Protocol) with Claude + +Using Claude Code as Orchestrator, MAS can access: +- Filesystem (read/write project files) +- Git (status, commit, push, PR) +- SSH (run remote commands on your servers) +- PostgreSQL (schema inspection, migrations, queries) +- Kubernetes (kubectl via MCP tool) + +This allows fully automated workflows like: +- β€œCreate a new service, add deployment manifests, and deploy to k3s.” +- β€œDebug failing pods and propose a fix, then open a PR.” + +### 2. Multi-agent collaboration (LangGraph) + +Typical workflow: + +```text +User request + ↓ +Claude Orchestrator + ↓ decides which agent(s) to call +Backend Dev β†’ Frontend Dev β†’ SRE + ↓ +Claude Orchestrator (review & summary) + ↓ +Final answer to user +``` + +Examples: +- Full‑stack feature (API + UI + monitoring) +- Infra rollout (Harbor, Tekton, CNPG, MetalLB) with validation + +--- + +## πŸ“ Usage examples + +### Backend API request + +```text +User: "Create a signup API with FastAPI. + Use PostgreSQL and JWT tokens." + +🎼 Orchestrator: + β†’ routes to Qwen Backend + +βš™οΈ Qwen Backend: + β†’ generates FastAPI router, Pydantic models, DB schema, JWT logic + +🎼 Orchestrator: + β†’ reviews, suggests improvements, and outputs final code snippet & file layout +``` + +### Frontend component request + +```text +User: "Build a responsive dashboard chart component using Recharts." + +🎼 Orchestrator: + β†’ routes to Qwen Frontend + +🎨 Qwen Frontend: + β†’ generates a Next.js/React component with TypeScript and responsive styles + +🎼 Orchestrator: + β†’ explains how to integrate it into your existing app +``` + +### Infra / SRE request + +```text +User: "Prometheus is firing high memory alerts for the PostgreSQL pod. + Help me stabilize it." + +🎼 Orchestrator: + β†’ routes to Qwen SRE + +πŸ“Š Qwen SRE: + β†’ analyzes metrics & logs (conceptually), + proposes tuning (Postgres config, indexes, pooler), + and suggests alert threshold adjustments. +``` + +--- + +## 🀝 Contributing + +Contributions are welcome: +- New agents (e.g., data engineer, security engineer) +- New tools (Harbor, Tekton, CNPG, MetalLB integrations) +- Better prompts and workflows +- Docs and examples + +Feel free to open issues or PRs in your Git repository. + +--- + +## πŸ“„ License + +MIT + diff --git a/deploy/argocd/mas.yaml b/deploy/argocd/mas.yaml new file mode 100644 index 0000000..279b05c --- /dev/null +++ b/deploy/argocd/mas.yaml @@ -0,0 +1,21 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: mas + namespace: argocd +spec: + project: default + source: + repoURL: https://gitea0213.kro.kr/bluemayne/mas.git + targetRevision: HEAD + path: deploy/k8s + destination: + server: https://kubernetes.default.svc + namespace: mas + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile new file mode 100644 index 0000000..ba53e71 --- /dev/null +++ b/deploy/docker/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +WORKDIR /app + +# μ‹œμŠ€ν…œ μ˜μ‘΄μ„± μ„€μΉ˜ +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Python μ˜μ‘΄μ„± μ„€μΉ˜ +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ½”λ“œ 볡사 +COPY . . + +# Chainlit 포트 +EXPOSE 8000 + +# Chainlit μ‹€ν–‰ +CMD ["chainlit", "run", "chainlit_app.py", "--host", "0.0.0.0", "--port", "8000"] + diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..8af1bf5 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,73 @@ +version: '3.8' + +services: + mas: + build: ../../services/backend + container_name: mas + ports: + - "8000:8000" + environment: + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + # Groq API (OpenAI-compatible) + - GROQ_API_KEY=${GROQ_API_KEY} + - GROQ_API_BASE=${GROQ_API_BASE:-https://api.groq.com/openai/v1} + # (optional) keep other providers + - OPENAI_API_KEY=${OPENAI_API_KEY} + - GOOGLE_API_KEY=${GOOGLE_API_KEY} + - DATABASE_URL=postgresql+asyncpg://mas:mas@postgres:5432/mas + - REDIS_URL=redis://redis:6379/0 + depends_on: + - redis + - postgres + - ollama + volumes: + - ../../services/backend:/app + networks: + - mas-network + + # Ollama (둜컬 Qwen λͺ¨λΈ) + ollama: + image: ollama/ollama:latest + container_name: mas-ollama + ports: + - "11434:11434" + volumes: + - ollama-data:/root/.ollama + networks: + - mas-network + + # PostgreSQL + postgres: + image: postgres:16-alpine + container_name: mas-postgres + environment: + POSTGRES_DB: mas + POSTGRES_USER: mas + POSTGRES_PASSWORD: mas + ports: + - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - mas-network + + # Redis + redis: + image: redis:7-alpine + container_name: mas-redis + ports: + - "6379:6379" + volumes: + - redis-data:/data + networks: + - mas-network + +volumes: + ollama-data: + postgres-data: + redis-data: + +networks: + mas-network: + driver: bridge + diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/deployment.yaml new file mode 100644 index 0000000..c108a70 --- /dev/null +++ b/deploy/k8s/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mas + namespace: mas + labels: + app: mas +spec: + replicas: 2 + selector: + matchLabels: + app: mas + template: + metadata: + labels: + app: mas + spec: + containers: + - name: mas + image: harbor.mayne.vcn/mas/platform:latest + ports: + - containerPort: 8000 + name: http + env: + - name: ANTHROPIC_API_KEY + valueFrom: + secretKeyRef: + name: mas-api-keys + key: anthropic-api-key + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: mas-api-keys + key: openai-api-key + - name: GOOGLE_API_KEY + valueFrom: + secretKeyRef: + name: mas-api-keys + key: google-api-key + - name: GROQ_API_KEY + valueFrom: + secretKeyRef: + name: mas-api-keys + key: groq-api-key + - name: GROQ_API_BASE + value: "https://api.groq.com/openai/v1" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: mas-postgres + key: database-url + - name: REDIS_URL + value: "redis://redis:6379/0" + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "2000m" + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 + diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/ingress.yaml new file mode 100644 index 0000000..80121f4 --- /dev/null +++ b/deploy/k8s/ingress.yaml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: mas + namespace: mas + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/websocket-services: "mas" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" +spec: + ingressClassName: nginx + tls: + - hosts: + - mas.mayne.vcn + secretName: mas-tls + rules: + - host: mas.mayne.vcn + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: mas + port: + number: 8000 + diff --git a/deploy/k8s/kustomization.yaml b/deploy/k8s/kustomization.yaml new file mode 100644 index 0000000..389615f --- /dev/null +++ b/deploy/k8s/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: mas + +resources: + - namespace.yaml + - ../vault/mas-api-keys.yaml + - ../vault/mas-postgres.yaml + - deployment.yaml + - service.yaml + - ingress.yaml + diff --git a/deploy/k8s/namespace.yaml b/deploy/k8s/namespace.yaml new file mode 100644 index 0000000..5424bf9 --- /dev/null +++ b/deploy/k8s/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: mas + labels: + name: mas + diff --git a/deploy/k8s/service.yaml b/deploy/k8s/service.yaml new file mode 100644 index 0000000..2dc580c --- /dev/null +++ b/deploy/k8s/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: mas + namespace: mas + labels: + app: mas +spec: + type: ClusterIP + ports: + - port: 8000 + targetPort: 8000 + protocol: TCP + name: http + selector: + app: mas + diff --git a/deploy/vault/mas-api-keys.yaml b/deploy/vault/mas-api-keys.yaml new file mode 100644 index 0000000..0f64780 --- /dev/null +++ b/deploy/vault/mas-api-keys.yaml @@ -0,0 +1,31 @@ +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: mas-api-keys + namespace: mas +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: mas-api-keys + creationPolicy: Owner + data: + - secretKey: anthropic-api-key + remoteRef: + key: mas/api-keys + property: ANTHROPIC_API_KEY + - secretKey: groq-api-key + remoteRef: + key: mas/api-keys + property: GROQ_API_KEY + - secretKey: openai-api-key + remoteRef: + key: mas/api-keys + property: OPENAI_API_KEY + - secretKey: google-api-key + remoteRef: + key: mas/api-keys + property: GOOGLE_API_KEY + diff --git a/deploy/vault/mas-postgres.yaml b/deploy/vault/mas-postgres.yaml new file mode 100644 index 0000000..2ce74c0 --- /dev/null +++ b/deploy/vault/mas-postgres.yaml @@ -0,0 +1,27 @@ +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: mas-postgres + namespace: mas +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-backend + target: + name: mas-postgres + creationPolicy: Owner + data: + - secretKey: database-url + remoteRef: + key: mas/postgres + property: DATABASE_URL + - secretKey: username + remoteRef: + key: mas/postgres + property: USERNAME + - secretKey: password + remoteRef: + key: mas/postgres + property: PASSWORD + diff --git a/services/backend/.chainlit b/services/backend/.chainlit new file mode 100644 index 0000000..aa62465 --- /dev/null +++ b/services/backend/.chainlit @@ -0,0 +1,17 @@ +[project] +enable_telemetry = false +user_env = [] +session_timeout = 3600 +cache = false + +[features] +prompt_playground = true +unsafe_allow_html = true +latex = true + +[UI] +name = "MAS Platform" +default_collapse_content = true +default_expand_messages = false +hide_cot = false + diff --git a/services/backend/agents.py b/services/backend/agents.py new file mode 100644 index 0000000..2ba85bf --- /dev/null +++ b/services/backend/agents.py @@ -0,0 +1,240 @@ +""" +MAS (Multi-Agent System) μ—μ΄μ „νŠΈ μ •μ˜ +""" +from typing import Annotated, Literal, TypedDict +from langchain_anthropic import ChatAnthropic +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, END +from langgraph.prebuilt import ToolNode +from langchain_core.messages import HumanMessage, SystemMessage +import os + + +class AgentState(TypedDict): + """μ—μ΄μ „νŠΈ κ°„ κ³΅μœ λ˜λŠ” μƒνƒœ""" + messages: list + current_agent: str + task_type: str + result: dict + + +# ===== 1. Claude Code - Orchestrator ===== +claude_orchestrator = ChatAnthropic( + model="claude-3-5-sonnet-20241022", + api_key=os.getenv("ANTHROPIC_API_KEY"), + temperature=0 +) + +ORCHESTRATOR_PROMPT = """당신은 MAS의 총괄 쑰율자이자 DevOps μ „λ¬Έκ°€μž…λ‹ˆλ‹€. + +**μ—­ν• **: +- μ‚¬μš©μž μš”μ²­μ„ λΆ„μ„ν•˜μ—¬ μ μ ˆν•œ μ—μ΄μ „νŠΈμ—κ²Œ μž‘μ—… ν• λ‹Ή +- Kubernetes, ArgoCD, Helm, Kustomize 관리 +- CI/CD νŒŒμ΄ν”„λΌμΈ ꡬ성 +- μ΅œμ’… μ½”λ“œ 리뷰 및 승인 + +**μ‚¬μš© κ°€λŠ₯ν•œ μ—μ΄μ „νŠΈ**: +1. backend_developer: FastAPI, Node.js λ°±μ—”λ“œ 개발 +2. frontend_developer: Next.js, React ν”„λ‘ νŠΈμ—”λ“œ 개발 +3. sre_specialist: λͺ¨λ‹ˆν„°λ§, μ„±λŠ₯ μ΅œμ ν™”, λ³΄μ•ˆ + +μš”μ²­μ„ λΆ„μ„ν•˜κ³  μ–΄λ–€ μ—μ΄μ „νŠΈκ°€ μ²˜λ¦¬ν•΄μ•Ό ν• μ§€ κ²°μ •ν•˜μ„Έμš”. +""" + + +# ===== 2. Groq #1 - Backend Developer ===== +# Groq OpenAI-compatible endpoint +GROQ_API_BASE = os.getenv("GROQ_API_BASE", "https://api.groq.com/openai/v1") +GROQ_API_KEY = os.getenv("GROQ_API_KEY", "") + +groq_backend = ChatOpenAI( + model=os.getenv("GROQ_BACKEND_MODEL", "llama-3.3-70b-specdec"), + base_url=GROQ_API_BASE, + api_key=GROQ_API_KEY, + temperature=0.7, +) + +BACKEND_PROMPT = """당신은 λ°±μ—”λ“œ 개발 μ „λ¬Έκ°€μž…λ‹ˆλ‹€. + +**μ—­ν• **: +- FastAPI, Node.js λ°±μ—”λ“œ 개발 +- REST API 섀계 및 κ΅¬ν˜„ +- λ°μ΄ν„°λ² μ΄μŠ€ 쿼리 μ΅œμ ν™” +- λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 κ΅¬ν˜„ + +μš”μ²­λœ λ°±μ—”λ“œ μž‘μ—…μ„ μˆ˜ν–‰ν•˜κ³  μ½”λ“œλ₯Ό μƒμ„±ν•˜μ„Έμš”. +""" + + +# ===== 3. Groq #2 - Frontend Developer ===== +groq_frontend = ChatOpenAI( + model=os.getenv("GROQ_FRONTEND_MODEL", "llama-3.1-8b-instant"), + base_url=GROQ_API_BASE, + api_key=GROQ_API_KEY, + temperature=0.7, +) + +FRONTEND_PROMPT = """당신은 ν”„λ‘ νŠΈμ—”λ“œ 개발 μ „λ¬Έκ°€μž…λ‹ˆλ‹€. + +**μ—­ν• **: +- Next.js, React μ»΄ν¬λ„ŒνŠΈ 개발 +- UI/UX κ΅¬ν˜„ +- μƒνƒœ 관리 +- λ°˜μ‘ν˜• λ””μžμΈ + +μš”μ²­λœ ν”„λ‘ νŠΈμ—”λ“œ μž‘μ—…μ„ μˆ˜ν–‰ν•˜κ³  μ½”λ“œλ₯Ό μƒμ„±ν•˜μ„Έμš”. +""" + + +# ===== 4. Groq #3 - SRE Specialist ===== +groq_sre = ChatOpenAI( + model=os.getenv("GROQ_SRE_MODEL", "llama-3.1-8b-instant"), + base_url=GROQ_API_BASE, + api_key=GROQ_API_KEY, + temperature=0.3, +) + +SRE_PROMPT = """당신은 SRE(Site Reliability Engineer) μ „λ¬Έκ°€μž…λ‹ˆλ‹€. + +**μ—­ν• **: +- μ‹œμŠ€ν…œ λͺ¨λ‹ˆν„°λ§ (Prometheus, Grafana, Loki) +- 둜그 뢄석 및 μ•ŒλžŒ μ„€μ • +- μ„±λŠ₯ νŠœλ‹ +- λ³΄μ•ˆ 취약점 점검 + +μš”μ²­λœ SRE μž‘μ—…μ„ μˆ˜ν–‰ν•˜κ³  μ†”λ£¨μ…˜μ„ μ œμ‹œν•˜μ„Έμš”. +""" + + +def orchestrator_node(state: AgentState) -> AgentState: + """Claude Code - μž‘μ—… 뢄석 및 ν• λ‹Ή""" + messages = state["messages"] + + response = claude_orchestrator.invoke([ + SystemMessage(content=ORCHESTRATOR_PROMPT), + HumanMessage(content=messages[-1]["content"]) + ]) + + # μž‘μ—… νƒ€μž… κ²°μ • + content = response.content.lower() + if "backend" in content or "api" in content or "fastapi" in content: + next_agent = "backend_developer" + elif "frontend" in content or "ui" in content or "react" in content: + next_agent = "frontend_developer" + elif "monitoring" in content or "performance" in content or "sre" in content: + next_agent = "sre_specialist" + else: + next_agent = "orchestrator" # μžμ‹ μ΄ 직접 처리 + + state["messages"].append({ + "role": "orchestrator", + "content": response.content + }) + state["current_agent"] = next_agent + + return state + + +def backend_node(state: AgentState) -> AgentState: + """Groq #1 - λ°±μ—”λ“œ 개발""" + messages = state["messages"] + + response = groq_backend.invoke([ + SystemMessage(content=BACKEND_PROMPT), + HumanMessage(content=messages[-1]["content"]) + ]) + + state["messages"].append({ + "role": "backend_developer", + "content": response.content + }) + state["current_agent"] = "orchestrator" # κ²°κ³Όλ₯Ό μ˜€μΌ€μŠ€νŠΈλ ˆμ΄ν„°μ—κ²Œ λ°˜ν™˜ + + return state + + +def frontend_node(state: AgentState) -> AgentState: + """Groq #2 - ν”„λ‘ νŠΈμ—”λ“œ 개발""" + messages = state["messages"] + + response = groq_frontend.invoke([ + SystemMessage(content=FRONTEND_PROMPT), + HumanMessage(content=messages[-1]["content"]) + ]) + + state["messages"].append({ + "role": "frontend_developer", + "content": response.content + }) + state["current_agent"] = "orchestrator" + + return state + + +def sre_node(state: AgentState) -> AgentState: + """Groq #3 - SRE μž‘μ—…""" + messages = state["messages"] + + response = groq_sre.invoke([ + SystemMessage(content=SRE_PROMPT), + HumanMessage(content=messages[-1]["content"]) + ]) + + state["messages"].append({ + "role": "sre_specialist", + "content": response.content + }) + state["current_agent"] = "orchestrator" + + return state + + +def router(state: AgentState) -> Literal["backend_developer", "frontend_developer", "sre_specialist", "end"]: + """λ‹€μŒ μ—μ΄μ „νŠΈ λΌμš°νŒ…""" + current = state.get("current_agent", "orchestrator") + + if current == "backend_developer": + return "backend_developer" + elif current == "frontend_developer": + return "frontend_developer" + elif current == "sre_specialist": + return "sre_specialist" + else: + return "end" + + +# ===== LangGraph μ›Œν¬ν”Œλ‘œμš° ꡬ성 ===== +def create_mas_graph(): + """MAS μ›Œν¬ν”Œλ‘œμš° κ·Έλž˜ν”„ 생성""" + workflow = StateGraph(AgentState) + + # λ…Έλ“œ μΆ”κ°€ + workflow.add_node("orchestrator", orchestrator_node) + workflow.add_node("backend_developer", backend_node) + workflow.add_node("frontend_developer", frontend_node) + workflow.add_node("sre_specialist", sre_node) + + # μ—£μ§€ μ •μ˜ + workflow.set_entry_point("orchestrator") + workflow.add_conditional_edges( + "orchestrator", + router, + { + "backend_developer": "backend_developer", + "frontend_developer": "frontend_developer", + "sre_specialist": "sre_specialist", + "end": END + } + ) + + # 각 μ—μ΄μ „νŠΈλŠ” μž‘μ—… ν›„ orchestrator둜 볡귀 + workflow.add_edge("backend_developer", "orchestrator") + workflow.add_edge("frontend_developer", "orchestrator") + workflow.add_edge("sre_specialist", "orchestrator") + + return workflow.compile() + + +# κ·Έλž˜ν”„ μΈμŠ€ν„΄μŠ€ 생성 +mas_graph = create_mas_graph() + diff --git a/services/backend/chainlit_app.py b/services/backend/chainlit_app.py new file mode 100644 index 0000000..b501f7b --- /dev/null +++ b/services/backend/chainlit_app.py @@ -0,0 +1,85 @@ +""" +Chainlit UI for MAS Platform +""" +import chainlit as cl +from agents import mas_graph, AgentState +import os +from dotenv import load_dotenv + +load_dotenv() + + +@cl.on_chat_start +async def start(): + """μ±„νŒ… μ‹œμž‘ μ‹œ""" + await cl.Message( + content="πŸ€– **Multi-Agent System**에 μ˜€μ‹  것을 ν™˜μ˜ν•©λ‹ˆλ‹€!\n\n" + "μ €λŠ” λ‹€μŒ μ „λ¬Έκ°€ νŒ€κ³Ό ν•¨κ»˜ μž‘μ—…ν•©λ‹ˆλ‹€:\n\n" + "- 🎼 **Claude Code**: 총괄 쑰율자 & DevOps μ „λ¬Έκ°€\n" + "- βš™οΈ **Qwen Backend**: λ°±μ—”λ“œ 개발자\n" + "- 🎨 **Qwen Frontend**: ν”„λ‘ νŠΈμ—”λ“œ 개발자\n" + "- πŸ“Š **Qwen SRE**: λͺ¨λ‹ˆν„°λ§ & μ„±λŠ₯ μ „λ¬Έκ°€\n\n" + "무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?" + ).send() + + +@cl.on_message +async def main(message: cl.Message): + """λ©”μ‹œμ§€ μˆ˜μ‹  μ‹œ""" + + # 초기 μƒνƒœ + initial_state: AgentState = { + "messages": [{"role": "user", "content": message.content}], + "current_agent": "orchestrator", + "task_type": "", + "result": {} + } + + # 응닡 λ©”μ‹œμ§€ 생성 + response_msg = cl.Message(content="") + await response_msg.send() + + # MAS κ·Έλž˜ν”„ μ‹€ν–‰ + async for event in mas_graph.astream(initial_state): + for node_name, state in event.items(): + if node_name != "__end__": + last_message = state["messages"][-1] + agent_name = last_message["role"] + agent_content = last_message["content"] + + # μ—μ΄μ „νŠΈλ³„ μ•„μ΄μ½˜ + agent_icons = { + "orchestrator": "🎼", + "backend_developer": "βš™οΈ", + "frontend_developer": "🎨", + "sre_specialist": "πŸ“Š" + } + + icon = agent_icons.get(agent_name, "πŸ€–") + + # 슀트리밍 μ—…λ°μ΄νŠΈ + response_msg.content += f"\n\n{icon} **{agent_name}**:\n{agent_content}" + await response_msg.update() + + # μ΅œμ’… μ—…λ°μ΄νŠΈ + await response_msg.update() + + +@cl.on_settings_update +async def setup_agent(settings): + """μ„€μ • μ—…λ°μ΄νŠΈ""" + print(f"Settings updated: {settings}") + + +# μ‚¬μ΄λ“œλ°” μ„€μ • +@cl.author_rename +def rename(orig_author: str): + """μ—μ΄μ „νŠΈ 이름 λ§€ν•‘""" + rename_dict = { + "orchestrator": "Claude Code (Orchestrator)", + "backend_developer": "Qwen Backend Dev", + "frontend_developer": "Qwen Frontend Dev", + "sre_specialist": "Qwen SRE" + } + return rename_dict.get(orig_author, orig_author) + diff --git a/services/backend/requirements.txt b/services/backend/requirements.txt new file mode 100644 index 0000000..7e28df8 --- /dev/null +++ b/services/backend/requirements.txt @@ -0,0 +1,30 @@ +# LangGraph & LangChain +langgraph==0.2.53 +langchain==0.3.13 +langchain-anthropic==0.3.0 +langchain-openai==0.2.14 +langchain-google-genai==2.0.8 + +# Chainlit (UI) +chainlit==1.3.1 + +# API Framework +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +pydantic==2.10.5 +pydantic-settings==2.7.0 + +# Database +sqlalchemy==2.0.36 +asyncpg==0.30.0 +psycopg2-binary==2.9.10 + +# MCP (Model Context Protocol) +mcp==1.1.2 + +# Utilities +python-dotenv==1.0.1 +redis==5.2.1 +aioredis==2.0.1 +httpx==0.28.1 +