feat(ai-orch): Coordinator (Orchestrator-Worker) 패턴 구현
Build and Push Images / build-and-push (push) Has been cancelled
Build and Push Images / build-and-push (push) Has been cancelled
진짜 멀티 에이전트 오케스트레이션 — 라우터 + 워커 + 합성 3단계
Engine (multiAgentExecutionEngine.ts)
- execution_mode 'coordinator' 신규 추가
- executeCoordinator() 3 라운드 구현:
Round 1: coordinator 가 사용자 요청 분해 → JSON delegations 출력
system prompt 에 sub-agent 카탈로그(role/desc/connectors) 자동 inject
Round 2: delegated sub-agent 들 각자 짧은 task 만 받아 병렬 실행
Round 3: coordinator 가 모든 sub-agent 응답을 받아 사용자 친화적 최종 답변 합성
- coordinator 식별: member.config.is_coordinator === true 우선,
없으면 execution_order 가 가장 작은 멤버
- JSON 파싱 실패 시 모든 worker 에 원문 fallback
Service (aiAgentGroupService.ts)
- updateMember 에 config / is_coordinator 옵션 추가
is_coordinator 단축키는 기존 config jsonb 와 || 머지
UI (workspace/page.tsx)
- EXEC_MODES 에 'coordinator' (Compass 아이콘) 추가
- MemberCardContent 에 코디네이터 토글 버튼 (왕관 아이콘) — 클릭 즉시
updateGroupMember(is_coordinator) 호출
- 활성 시 분홍 배지 "코디네이터" 표시
검증 결과 (PLM 그룹 / 환율전문가=coordinator)
- 사용자: "환율과 화성시 날씨 알려줘"
Round 1: 기상전문가 ← "화성시의 날씨 정보를 알려줘"
Round 2: 기상전문가 → API 호출 → HTML 응답 → 정직하게 "데이터 못받음"
Round 3: coordinator 가 환율/날씨 모두 정리한 단일 답변 합성
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Network, Plus, Pencil, Trash2, Loader2, Bot, Database, Globe, FileText, Cpu, Bug, X, Zap, ListOrdered, Shuffle, GripVertical, ArrowDown, Check } from "lucide-react";
|
||||
import { Network, Plus, Pencil, Trash2, Loader2, Bot, Database, Globe, FileText, Cpu, Bug, X, Zap, ListOrdered, Shuffle, GripVertical, ArrowDown, Check, Crown, Compass } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy, useSortable } from "@dnd-kit/sortable";
|
||||
@@ -20,6 +20,7 @@ const EXEC_MODES = [
|
||||
{ value: "parallel", label: "병렬", desc: "모두 동시 실행", icon: Zap, color: "text-amber-500" },
|
||||
{ value: "sequential", label: "순차", desc: "1→2→3 순서대로", icon: ListOrdered, color: "text-blue-500" },
|
||||
{ value: "mixed", label: "혼합", desc: "같은 순서=병렬", icon: Shuffle, color: "text-violet-500" },
|
||||
{ value: "coordinator", label: "코디네이터", desc: "리더가 분배·합성", icon: Compass, color: "text-rose-500" },
|
||||
];
|
||||
|
||||
export default function WorkspacePage() {
|
||||
@@ -614,12 +615,25 @@ const CONNECTOR_TYPE_OPTIONS = [
|
||||
function MemberCardContent({ member, connectors, onRemove, onAddConnector, onRemoveConnector }: MemberCardProps) {
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [selectedType, setSelectedType] = useState<string | null>(null);
|
||||
const [isCoord, setIsCoord] = useState(!!member.config?.is_coordinator);
|
||||
const memberConnectors = member.connectors || [];
|
||||
|
||||
const filteredConnectors = selectedType
|
||||
? connectors.filter((c) => c.type === selectedType)
|
||||
: [];
|
||||
|
||||
const toggleCoordinator = async () => {
|
||||
const next = !isCoord;
|
||||
setIsCoord(next);
|
||||
try {
|
||||
await aiAgentApi.updateGroupMember(member.id, { is_coordinator: next });
|
||||
toast.success(next ? "코디네이터로 지정" : "코디네이터 해제");
|
||||
} catch (e: any) {
|
||||
setIsCoord(!next);
|
||||
toast.error(e?.response?.data?.message || "코디네이터 토글 실패");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
@@ -628,12 +642,30 @@ function MemberCardContent({ member, connectors, onRemove, onAddConnector, onRem
|
||||
<Bot className="h-3.5 w-3.5 text-primary" />
|
||||
<span className="text-xs font-semibold">{member.role_name}</span>
|
||||
<Badge variant="outline" className="h-4 text-[9px]">{member.agent_name || "에이전트"}</Badge>
|
||||
{isCoord && (
|
||||
<Badge className="h-4 gap-0.5 bg-rose-500/15 px-1 text-[9px] text-rose-700 hover:bg-rose-500/20">
|
||||
<Crown className="h-2.5 w-2.5" /> 코디네이터
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-0.5 ml-5 text-[10px] text-muted-foreground font-mono">{member.agent_model}</p>
|
||||
</div>
|
||||
<button className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-destructive hover:bg-destructive/10" onClick={onRemove}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={toggleCoordinator}
|
||||
className={`flex h-5 px-1.5 items-center justify-center rounded text-[9px] gap-0.5 transition-colors ${
|
||||
isCoord
|
||||
? "bg-rose-500/20 text-rose-700 hover:bg-rose-500/30"
|
||||
: "border border-dashed text-muted-foreground hover:border-rose-400 hover:text-rose-500"
|
||||
}`}
|
||||
title="이 에이전트를 그룹 코디네이터로 지정 (요청 분배·결과 합성 담당)"
|
||||
>
|
||||
<Crown className="h-2.5 w-2.5" /> 코디
|
||||
</button>
|
||||
<button className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-destructive hover:bg-destructive/10" onClick={onRemove}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 연결된 커넥터 */}
|
||||
|
||||
Reference in New Issue
Block a user