From c9a0563422728b1f1e7a3b2d678aca85e06d04d8 Mon Sep 17 00:00:00 2001 From: Jonas Pacheco Date: Wed, 25 Mar 2026 09:32:39 -0300 Subject: [PATCH] =?UTF-8?q?feat(agentic):=20Fase=201-2=20=E2=80=94=20Skill?= =?UTF-8?q?s=20POO,=20Neo4j,=20UI=20editor=20e=20melhorias=20gerais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arcádia Agentic Suite — Fase 1 (Fundação): - shared/schema.ts: tabelas arcadia_skills + skill_executions (multi-tenant, herança, audit hash) - docker-compose.yml: Neo4j 5 como profile 'kg' (porta 7474/7687) - docker/litellm-config.yaml: ajustes de configuração Arcádia Agentic Suite — Fase 2 (Skills Engine): - client/src/pages/Skills.tsx: UI completa — lista, filtros, editor Monaco (Geral/Body/Params), executar e histórico - client/src/App.tsx: import do componente Skills Manus — melhorias de prompt e comportamento: - server/manus/service.ts: system prompt com identidade Manus, regras PROATIVO, tabelas markdown - server/manus/routes.ts: ajustes de rotas - server/replit_integrations/chat/prompt.ts: prompt de chat aprimorado - server/replit_integrations/chat/routes.ts: melhorias no fluxo de chat Outros: - Agent.tsx, ProcessCompass.tsx: melhorias de UI - Correções menores em bi, blackboard, compass, ide, support, valuation, whatsapp Co-Authored-By: Claude Sonnet 4.6 --- client/src/App.tsx | 1 + client/src/pages/Agent.tsx | 74 ++- client/src/pages/ProcessCompass.tsx | 40 +- client/src/pages/Skills.tsx | 706 ++++++++++++++++++++++ docker-compose.yml | 18 + docker/litellm-config.yaml | 15 +- server/bi/routes.ts | 2 +- server/blackboard/BaseBlackboardAgent.ts | 2 +- server/compass/routes.ts | 11 +- server/devAgent.ts | 6 +- server/ide/routes.ts | 4 +- server/manus/routes.ts | 41 +- server/manus/service.ts | 102 +++- server/replit_integrations/chat/prompt.ts | 112 +++- server/replit_integrations/chat/routes.ts | 102 +++- server/support/routes.ts | 4 +- server/valuation/routes.ts | 10 +- server/whatsapp/service.ts | 2 +- shared/schema.ts | 96 +++ 19 files changed, 1251 insertions(+), 97 deletions(-) create mode 100644 client/src/pages/Skills.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 57a89c0..21ce75d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -70,6 +70,7 @@ const XosPipeline = lazy(() => import("@/pages/XosPipeline")); const XosSupervisor = lazy(() => import("@/pages/XosSupervisor")); const XosReports = lazy(() => import("@/pages/XosReports")); const XosProtocols = lazy(() => import("@/pages/XosProtocols")); +const Skills = lazy(() => import("@/pages/Skills")); function LoadingFallback() { diff --git a/client/src/pages/Agent.tsx b/client/src/pages/Agent.tsx index d736d22..0591444 100644 --- a/client/src/pages/Agent.tsx +++ b/client/src/pages/Agent.tsx @@ -365,6 +365,14 @@ async function cancelManusRun(id: number): Promise { await fetch(`/api/manus/runs/${id}`, { method: "DELETE", credentials: "include" }); } +async function deleteManusRun(id: number): Promise { + await fetch(`/api/manus/runs/${id}`, { method: "DELETE", credentials: "include" }); +} + +async function deleteAllManusRuns(): Promise { + await fetch("/api/manus/runs", { method: "DELETE", credentials: "include" }); +} + async function startManusRun(data: { prompt: string; attachedFiles?: AttachedFile[]; conversationHistory?: Array<{role: string; content: string}> }): Promise<{ runId: number }> { const response = await fetch("/api/manus/run", { method: "POST", @@ -1246,7 +1254,13 @@ export default function Agent() { if (line.startsWith("data: ")) { try { const data = JSON.parse(line.slice(6)); + if (data.tool_status) { + setCurrentToolName(data.tool_status); + setProcessingMode("using_tools"); + } if (data.content) { + setCurrentToolName(null); + setProcessingMode("idle"); fullContent += data.content; setStreamingContent(fullContent); } @@ -1485,8 +1499,21 @@ export default function Agent() { Nova Tarefa -
+

Todas as Tarefas

+ {manusRuns.length > 0 && ( + + )}
{loadingRuns ? ( @@ -1498,22 +1525,35 @@ export default function Agent() { ) : (
{manusRuns.map((run) => ( - +
+ + +
))}
)} diff --git a/client/src/pages/ProcessCompass.tsx b/client/src/pages/ProcessCompass.tsx index 06d1123..2bc37b2 100644 --- a/client/src/pages/ProcessCompass.tsx +++ b/client/src/pages/ProcessCompass.tsx @@ -1855,7 +1855,7 @@ export default function ProcessCompass() { data-testid="search-projects" />
- @@ -4600,18 +4600,32 @@ export default function ProcessCompass() {
- + {clients.length === 0 ? ( +
+ Nenhum cliente cadastrado.{" "} + {" "} + antes de criar um projeto. +
+ ) : ( + + )}
diff --git a/client/src/pages/Skills.tsx b/client/src/pages/Skills.tsx new file mode 100644 index 0000000..c187d17 --- /dev/null +++ b/client/src/pages/Skills.tsx @@ -0,0 +1,706 @@ +import { BrowserFrame } from "@/components/Browser/BrowserFrame"; +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Plus, + Play, + Trash2, + Pencil, + Loader2, + CheckCircle, + XCircle, + Clock, + Search, + Zap, + Code2, + ListChecks, + ChevronRight, + History, + Copy, +} from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; +import Editor from "@monaco-editor/react"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface Skill { + id: string; + name: string; + slug: string; + description: string | null; + namespace: string; + status: string; + triggerType: string | null; + body: string | null; + parametersSchema: Record | null; + tags: string[] | null; + extends: string[] | null; + createdAt: string; + updatedAt: string; +} + +interface SkillExecution { + id: string; + skillId: string; + status: string; + triggeredBy: string; + inputParams: Record | null; + outputResult: Record | null; + errorMessage: string | null; + durationMs: number | null; + startedAt: string; + completedAt: string | null; +} + +const EMPTY_SKILL = { + name: "", + slug: "", + description: "", + namespace: "tenant", + status: "draft", + triggerType: "manual", + body: "# Skill\n\nDescreva o comportamento desta skill aqui.\n\n## Entradas\n\n/var/input\n\n## Processamento\n\n...\n\n## Saída\n\n...", + parametersSchema: "{}", + tags: "", + extends: "", +}; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function statusBadge(status: string) { + const map: Record = { + active: "bg-green-500/20 text-green-400 border-green-500/30", + draft: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30", + archived: "bg-gray-500/20 text-gray-400 border-gray-500/30", + }; + return map[status] ?? map.draft; +} + +function execStatusIcon(status: string) { + if (status === "success") return ; + if (status === "error") return ; + return ; +} + +function toSlug(name: string) { + return name + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_|_$/g, ""); +} + +// ─── Main Component ─────────────────────────────────────────────────────────── + +export default function Skills() { + const qc = useQueryClient(); + const { toast } = useToast(); + const [search, setSearch] = useState(""); + const [nsFilter, setNsFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); + + const [editOpen, setEditOpen] = useState(false); + const [editingSkill, setEditingSkill] = useState(null); + const [form, setForm] = useState(EMPTY_SKILL); + + const [execOpen, setExecOpen] = useState(false); + const [execSkill, setExecSkill] = useState(null); + const [execParams, setExecParams] = useState("{}"); + const [execResult, setExecResult] = useState(null); + + const [historyOpen, setHistoryOpen] = useState(false); + const [historySkill, setHistorySkill] = useState(null); + + // ── Queries ──────────────────────────────────────────────────────────────── + + const { data: skillsData, isLoading } = useQuery<{ skills: Skill[] }>({ + queryKey: ["/api/skills", search, nsFilter, statusFilter], + queryFn: async () => { + const params = new URLSearchParams(); + if (search) params.set("search", search); + if (nsFilter !== "all") params.set("namespace", nsFilter); + if (statusFilter !== "all") params.set("status", statusFilter); + const res = await fetch(`/api/skills?${params}`); + return res.json(); + }, + }); + + const { data: historyData } = useQuery<{ executions: SkillExecution[] }>({ + queryKey: ["/api/skills", historySkill?.id, "executions"], + queryFn: async () => { + const res = await fetch(`/api/skills/${historySkill!.id}/executions?limit=20`); + return res.json(); + }, + enabled: !!historySkill && historyOpen, + }); + + // ── Mutations ────────────────────────────────────────────────────────────── + + const saveMutation = useMutation({ + mutationFn: async (data: typeof form) => { + let paramsSchema: Record = {}; + try { paramsSchema = JSON.parse(data.parametersSchema); } catch {} + + const payload = { + name: data.name, + slug: data.slug || toSlug(data.name), + description: data.description || null, + namespace: data.namespace, + status: data.status, + triggerType: data.triggerType, + body: data.body, + parametersSchema: paramsSchema, + tags: data.tags ? data.tags.split(",").map(t => t.trim()).filter(Boolean) : [], + extends: data.extends ? data.extends.split(",").map(t => t.trim()).filter(Boolean) : [], + }; + + const url = editingSkill ? `/api/skills/${editingSkill.id}` : "/api/skills"; + const method = editingSkill ? "PUT" : "POST"; + const res = await fetch(url, { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["/api/skills"] }); + setEditOpen(false); + toast({ title: editingSkill ? "Skill atualizada" : "Skill criada" }); + }, + onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), + }); + + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + const res = await fetch(`/api/skills/${id}`, { method: "DELETE" }); + if (!res.ok) throw new Error("Erro ao deletar"); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["/api/skills"] }); + toast({ title: "Skill removida" }); + }, + onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), + }); + + const executeMutation = useMutation({ + mutationFn: async ({ skillId, params }: { skillId: string; params: string }) => { + let inputParams: Record = {}; + try { inputParams = JSON.parse(params); } catch {} + const res = await fetch(`/api/skills/${skillId}/execute`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ inputParams, triggeredBy: "manual" }), + }); + return res.json() as Promise; + }, + onSuccess: (data) => { + setExecResult(data); + qc.invalidateQueries({ queryKey: ["/api/skills"] }); + }, + onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), + }); + + // ── Handlers ─────────────────────────────────────────────────────────────── + + function openNew() { + setEditingSkill(null); + setForm(EMPTY_SKILL); + setEditOpen(true); + } + + function openEdit(skill: Skill) { + setEditingSkill(skill); + setForm({ + name: skill.name, + slug: skill.slug, + description: skill.description ?? "", + namespace: skill.namespace, + status: skill.status, + triggerType: skill.triggerType ?? "manual", + body: skill.body ?? "", + parametersSchema: JSON.stringify(skill.parametersSchema ?? {}, null, 2), + tags: (skill.tags ?? []).join(", "), + extends: (skill.extends ?? []).join(", "), + }); + setEditOpen(true); + } + + function openExec(skill: Skill) { + setExecSkill(skill); + setExecParams("{}"); + setExecResult(null); + setExecOpen(true); + } + + function openHistory(skill: Skill) { + setHistorySkill(skill); + setHistoryOpen(true); + } + + const skills = skillsData?.skills ?? []; + + // ── Render ───────────────────────────────────────────────────────────────── + + return ( + +
+ + {/* Header */} +
+
+

Skills

+

Objetos reutilizáveis — herança, composição, polimorfismo

+
+ +
+ + {/* Filters */} +
+
+ + setSearch(e.target.value)} + className="pl-8 h-8 text-sm bg-white/5 border-white/10" + /> +
+ + +
+ + {/* List */} + + {isLoading ? ( +
+ +
+ ) : skills.length === 0 ? ( +
+ +

Nenhuma skill encontrada

+ +
+ ) : ( +
+ {skills.map(skill => ( + + +
+
+
+ {skill.name} + + {skill.status} + + + {skill.namespace} + +
+

/skill/{skill.namespace}/{skill.slug}

+ {skill.description && ( +

{skill.description}

+ )} + {skill.tags && skill.tags.length > 0 && ( +
+ {skill.tags.map(tag => ( + {tag} + ))} +
+ )} +
+
+ + + + +
+
+
+
+ ))} +
+ )} +
+
+ + {/* ── Edit / Create Dialog ────────────────────────────────────────────── */} + + + + {editingSkill ? "Editar Skill" : "Nova Skill"} + + + + + + Geral + + + Body + + + Parâmetros + + + + {/* Tab: Geral */} + +
+
+ + setForm(f => ({ ...f, name: e.target.value, slug: toSlug(e.target.value) }))} + placeholder="Relatório de Vendas" + className="bg-white/5 border-white/10 text-sm" + /> +
+
+ + setForm(f => ({ ...f, slug: e.target.value }))} + placeholder="relatorio_vendas" + className="bg-white/5 border-white/10 text-sm font-mono" + /> +
+
+ +
+ +