feat(agentic): Fase 1-2 — Skills POO, Neo4j, UI editor e melhorias gerais

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 <noreply@anthropic.com>
This commit is contained in:
Jonas Pacheco 2026-03-25 09:32:39 -03:00
parent 3c61acf4d9
commit c9a0563422
19 changed files with 1251 additions and 97 deletions

View File

@ -70,6 +70,7 @@ const XosPipeline = lazy(() => import("@/pages/XosPipeline"));
const XosSupervisor = lazy(() => import("@/pages/XosSupervisor")); const XosSupervisor = lazy(() => import("@/pages/XosSupervisor"));
const XosReports = lazy(() => import("@/pages/XosReports")); const XosReports = lazy(() => import("@/pages/XosReports"));
const XosProtocols = lazy(() => import("@/pages/XosProtocols")); const XosProtocols = lazy(() => import("@/pages/XosProtocols"));
const Skills = lazy(() => import("@/pages/Skills"));
function LoadingFallback() { function LoadingFallback() {

View File

@ -365,6 +365,14 @@ async function cancelManusRun(id: number): Promise<void> {
await fetch(`/api/manus/runs/${id}`, { method: "DELETE", credentials: "include" }); await fetch(`/api/manus/runs/${id}`, { method: "DELETE", credentials: "include" });
} }
async function deleteManusRun(id: number): Promise<void> {
await fetch(`/api/manus/runs/${id}`, { method: "DELETE", credentials: "include" });
}
async function deleteAllManusRuns(): Promise<void> {
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 }> { async function startManusRun(data: { prompt: string; attachedFiles?: AttachedFile[]; conversationHistory?: Array<{role: string; content: string}> }): Promise<{ runId: number }> {
const response = await fetch("/api/manus/run", { const response = await fetch("/api/manus/run", {
method: "POST", method: "POST",
@ -1246,7 +1254,13 @@ export default function Agent() {
if (line.startsWith("data: ")) { if (line.startsWith("data: ")) {
try { try {
const data = JSON.parse(line.slice(6)); const data = JSON.parse(line.slice(6));
if (data.tool_status) {
setCurrentToolName(data.tool_status);
setProcessingMode("using_tools");
}
if (data.content) { if (data.content) {
setCurrentToolName(null);
setProcessingMode("idle");
fullContent += data.content; fullContent += data.content;
setStreamingContent(fullContent); setStreamingContent(fullContent);
} }
@ -1485,8 +1499,21 @@ export default function Agent() {
Nova Tarefa Nova Tarefa
</Button> </Button>
</div> </div>
<div className="px-3 pt-3 pb-1"> <div className="px-3 pt-3 pb-1 flex items-center justify-between">
<p className="text-xs text-white/40 uppercase tracking-wider font-medium">Todas as Tarefas</p> <p className="text-xs text-white/40 uppercase tracking-wider font-medium">Todas as Tarefas</p>
{manusRuns.length > 0 && (
<button
onClick={async () => {
await deleteAllManusRuns();
setSelectedRun(null);
queryClient.invalidateQueries({ queryKey: ["manus-runs"] });
}}
className="text-[10px] text-white/30 hover:text-red-400 transition-colors"
title="Limpar histórico"
>
Limpar tudo
</button>
)}
</div> </div>
<ScrollArea className="flex-1 px-2"> <ScrollArea className="flex-1 px-2">
{loadingRuns ? ( {loadingRuns ? (
@ -1498,22 +1525,35 @@ export default function Agent() {
) : ( ) : (
<div className="space-y-1 pb-2"> <div className="space-y-1 pb-2">
{manusRuns.map((run) => ( {manusRuns.map((run) => (
<button <div key={run.id} className="group relative">
key={run.id} <button
onClick={() => setSelectedRun(run.id)} onClick={() => setSelectedRun(run.id)}
className={`w-full text-left p-2.5 rounded-lg transition-all ${ className={`w-full text-left p-2.5 rounded-lg transition-all pr-8 ${
selectedRun === run.id selectedRun === run.id
? "bg-[#c89b3c]/20 border border-[#c89b3c]/50" ? "bg-[#c89b3c]/20 border border-[#c89b3c]/50"
: "hover:bg-[#162638] border border-transparent" : "hover:bg-[#162638] border border-transparent"
}`} }`}
data-testid={`run-${run.id}`} data-testid={`run-${run.id}`}
> >
<p className="text-xs text-white line-clamp-2 mb-1.5">{run.prompt}</p> <p className="text-xs text-white line-clamp-2 mb-1.5">{run.prompt}</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getStatusBadge(run.status)} {getStatusBadge(run.status)}
<span className="text-[10px] text-white/40">{formatTime(run.createdAt)}</span> <span className="text-[10px] text-white/40">{formatTime(run.createdAt)}</span>
</div> </div>
</button> </button>
<button
onClick={async (e) => {
e.stopPropagation();
await deleteManusRun(run.id);
if (selectedRun === run.id) setSelectedRun(null);
queryClient.invalidateQueries({ queryKey: ["manus-runs"] });
}}
className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 text-white/30 hover:text-red-400 transition-all"
title="Excluir"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
))} ))}
</div> </div>
)} )}

View File

@ -1855,7 +1855,7 @@ export default function ProcessCompass() {
data-testid="search-projects" data-testid="search-projects"
/> />
</div> </div>
<Button onClick={() => setShowNewProjectDialog(true)} disabled={clients.length === 0} data-testid="btn-new-project"> <Button onClick={() => setShowNewProjectDialog(true)} data-testid="btn-new-project">
<Plus className="h-4 w-4 mr-2" /> Novo Projeto <Plus className="h-4 w-4 mr-2" /> Novo Projeto
</Button> </Button>
</div> </div>
@ -4600,18 +4600,32 @@ export default function ProcessCompass() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="project-client">Cliente</Label> <Label htmlFor="project-client">Cliente</Label>
<Select name="clientId" required> {clients.length === 0 ? (
<SelectTrigger data-testid="select-project-client"> <div className="rounded-md border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-800">
<SelectValue placeholder="Selecione um cliente" /> Nenhum cliente cadastrado.{" "}
</SelectTrigger> <button
<SelectContent> type="button"
{clients.map(client => ( className="underline font-medium"
<SelectItem key={client.id} value={client.id.toString()}> onClick={() => { setShowNewProjectDialog(false); setShowNewClientDialog(true); }}
{client.name} >
</SelectItem> Criar cliente
))} </button>{" "}
</SelectContent> antes de criar um projeto.
</Select> </div>
) : (
<Select name="clientId" required>
<SelectTrigger data-testid="select-project-client">
<SelectValue placeholder="Selecione um cliente" />
</SelectTrigger>
<SelectContent>
{clients.map(client => (
<SelectItem key={client.id} value={client.id.toString()}>
{client.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="project-description">Descrição</Label> <Label htmlFor="project-description">Descrição</Label>

706
client/src/pages/Skills.tsx Normal file
View File

@ -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<string, unknown> | null;
tags: string[] | null;
extends: string[] | null;
createdAt: string;
updatedAt: string;
}
interface SkillExecution {
id: string;
skillId: string;
status: string;
triggeredBy: string;
inputParams: Record<string, unknown> | null;
outputResult: Record<string, unknown> | 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<string, string> = {
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 <CheckCircle className="w-4 h-4 text-green-400" />;
if (status === "error") return <XCircle className="w-4 h-4 text-red-400" />;
return <Clock className="w-4 h-4 text-yellow-400 animate-spin" />;
}
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<Skill | null>(null);
const [form, setForm] = useState(EMPTY_SKILL);
const [execOpen, setExecOpen] = useState(false);
const [execSkill, setExecSkill] = useState<Skill | null>(null);
const [execParams, setExecParams] = useState("{}");
const [execResult, setExecResult] = useState<SkillExecution | null>(null);
const [historyOpen, setHistoryOpen] = useState(false);
const [historySkill, setHistorySkill] = useState<Skill | null>(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<string, unknown> = {};
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<string, unknown> = {};
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<SkillExecution>;
},
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 (
<BrowserFrame title="Skills" icon="⚡">
<div className="flex flex-col h-full bg-[#0a0f1a] text-white">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div>
<h1 className="text-lg font-semibold">Skills</h1>
<p className="text-xs text-muted-foreground">Objetos reutilizáveis herança, composição, polimorfismo</p>
</div>
<Button size="sm" onClick={openNew} className="bg-[#c89b3c] hover:bg-[#d4a94a] text-black">
<Plus className="w-4 h-4 mr-1" /> Nova Skill
</Button>
</div>
{/* Filters */}
<div className="flex gap-2 p-3 border-b border-white/10">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 w-3.5 h-3.5 text-muted-foreground" />
<Input
placeholder="Buscar skills..."
value={search}
onChange={e => setSearch(e.target.value)}
className="pl-8 h-8 text-sm bg-white/5 border-white/10"
/>
</div>
<Select value={nsFilter} onValueChange={setNsFilter}>
<SelectTrigger className="w-32 h-8 text-xs bg-white/5 border-white/10">
<SelectValue placeholder="Namespace" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="system">System</SelectItem>
<SelectItem value="tenant">Tenant</SelectItem>
<SelectItem value="company">Company</SelectItem>
<SelectItem value="user">User</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-28 h-8 text-xs bg-white/5 border-white/10">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="active">Ativo</SelectItem>
<SelectItem value="draft">Rascunho</SelectItem>
<SelectItem value="archived">Arquivado</SelectItem>
</SelectContent>
</Select>
</div>
{/* List */}
<ScrollArea className="flex-1">
{isLoading ? (
<div className="flex items-center justify-center h-40">
<Loader2 className="w-6 h-6 animate-spin text-[#c89b3c]" />
</div>
) : skills.length === 0 ? (
<div className="flex flex-col items-center justify-center h-40 text-muted-foreground gap-2">
<Zap className="w-8 h-8 opacity-30" />
<p className="text-sm">Nenhuma skill encontrada</p>
<Button size="sm" variant="outline" onClick={openNew}>Criar primeira skill</Button>
</div>
) : (
<div className="p-3 space-y-2">
{skills.map(skill => (
<Card key={skill.id} className="bg-white/5 border-white/10 hover:bg-white/8 transition-colors">
<CardContent className="p-3">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-sm truncate">{skill.name}</span>
<Badge variant="outline" className={`text-[10px] px-1.5 py-0 ${statusBadge(skill.status)}`}>
{skill.status}
</Badge>
<Badge variant="outline" className="text-[10px] px-1.5 py-0 border-white/20 text-white/50">
{skill.namespace}
</Badge>
</div>
<p className="text-xs text-muted-foreground font-mono">/skill/{skill.namespace}/{skill.slug}</p>
{skill.description && (
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">{skill.description}</p>
)}
{skill.tags && skill.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1.5">
{skill.tags.map(tag => (
<span key={tag} className="text-[10px] bg-white/10 px-1.5 py-0.5 rounded">{tag}</span>
))}
</div>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<Button
size="icon"
variant="ghost"
className="h-7 w-7 hover:bg-green-500/20 hover:text-green-400"
onClick={() => openExec(skill)}
title="Executar"
>
<Play className="w-3.5 h-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 hover:bg-blue-500/20 hover:text-blue-400"
onClick={() => openHistory(skill)}
title="Histórico"
>
<History className="w-3.5 h-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 hover:bg-white/10"
onClick={() => openEdit(skill)}
title="Editar"
>
<Pencil className="w-3.5 h-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 hover:bg-red-500/20 hover:text-red-400"
onClick={() => deleteMutation.mutate(skill.id)}
title="Deletar"
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</ScrollArea>
</div>
{/* ── Edit / Create Dialog ────────────────────────────────────────────── */}
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="max-w-3xl h-[85vh] flex flex-col bg-[#0f1729] border-white/10 text-white">
<DialogHeader>
<DialogTitle>{editingSkill ? "Editar Skill" : "Nova Skill"}</DialogTitle>
</DialogHeader>
<Tabs defaultValue="form" className="flex-1 flex flex-col min-h-0">
<TabsList className="bg-white/5 border border-white/10 w-fit">
<TabsTrigger value="form" className="text-xs gap-1">
<ListChecks className="w-3.5 h-3.5" /> Geral
</TabsTrigger>
<TabsTrigger value="body" className="text-xs gap-1">
<Code2 className="w-3.5 h-3.5" /> Body
</TabsTrigger>
<TabsTrigger value="params" className="text-xs gap-1">
<ChevronRight className="w-3.5 h-3.5" /> Parâmetros
</TabsTrigger>
</TabsList>
{/* Tab: Geral */}
<TabsContent value="form" className="flex-1 overflow-auto mt-3 space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs">Nome *</Label>
<Input
value={form.name}
onChange={e => 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"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Slug</Label>
<Input
value={form.slug}
onChange={e => setForm(f => ({ ...f, slug: e.target.value }))}
placeholder="relatorio_vendas"
className="bg-white/5 border-white/10 text-sm font-mono"
/>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs">Descrição</Label>
<Textarea
value={form.description}
onChange={e => setForm(f => ({ ...f, description: e.target.value }))}
rows={2}
className="bg-white/5 border-white/10 text-sm resize-none"
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-1">
<Label className="text-xs">Namespace</Label>
<Select value={form.namespace} onValueChange={v => setForm(f => ({ ...f, namespace: v }))}>
<SelectTrigger className="bg-white/5 border-white/10 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="system">system</SelectItem>
<SelectItem value="tenant">tenant</SelectItem>
<SelectItem value="company">company</SelectItem>
<SelectItem value="user">user</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">Status</Label>
<Select value={form.status} onValueChange={v => setForm(f => ({ ...f, status: v }))}>
<SelectTrigger className="bg-white/5 border-white/10 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">draft</SelectItem>
<SelectItem value="active">active</SelectItem>
<SelectItem value="archived">archived</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">Trigger</Label>
<Select value={form.triggerType} onValueChange={v => setForm(f => ({ ...f, triggerType: v }))}>
<SelectTrigger className="bg-white/5 border-white/10 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">manual</SelectItem>
<SelectItem value="schedule">schedule</SelectItem>
<SelectItem value="event">event</SelectItem>
<SelectItem value="webhook">webhook</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs">Tags (separadas por vírgula)</Label>
<Input
value={form.tags}
onChange={e => setForm(f => ({ ...f, tags: e.target.value }))}
placeholder="financeiro, relatório, automático"
className="bg-white/5 border-white/10 text-sm"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Herança extends (slugs separados por vírgula)</Label>
<Input
value={form.extends}
onChange={e => setForm(f => ({ ...f, extends: e.target.value }))}
placeholder="/skill/system/base_report, /skill/tenant/financeiro"
className="bg-white/5 border-white/10 text-sm font-mono"
/>
</div>
</TabsContent>
{/* Tab: Body (Monaco) */}
<TabsContent value="body" className="flex-1 min-h-0 mt-3">
<div className="h-full rounded border border-white/10 overflow-hidden">
<Editor
height="100%"
defaultLanguage="markdown"
value={form.body}
onChange={v => setForm(f => ({ ...f, body: v ?? "" }))}
theme="vs-dark"
options={{
fontSize: 13,
minimap: { enabled: false },
wordWrap: "on",
lineNumbers: "on",
scrollBeyondLastLine: false,
padding: { top: 8 },
}}
/>
</div>
</TabsContent>
{/* Tab: Params (JSON Schema) */}
<TabsContent value="params" className="flex-1 min-h-0 mt-3">
<div className="space-y-2 h-full flex flex-col">
<p className="text-xs text-muted-foreground">
JSON Schema dos parâmetros de entrada. Referenciáveis no body como <code className="bg-white/10 px-1 rounded">/var/nome_param</code>.
</p>
<div className="flex-1 rounded border border-white/10 overflow-hidden">
<Editor
height="100%"
defaultLanguage="json"
value={form.parametersSchema}
onChange={v => setForm(f => ({ ...f, parametersSchema: v ?? "{}" }))}
theme="vs-dark"
options={{
fontSize: 13,
minimap: { enabled: false },
formatOnType: true,
scrollBeyondLastLine: false,
padding: { top: 8 },
}}
/>
</div>
</div>
</TabsContent>
</Tabs>
<div className="flex justify-end gap-2 pt-3 border-t border-white/10 mt-2">
<Button variant="ghost" onClick={() => setEditOpen(false)}>Cancelar</Button>
<Button
onClick={() => saveMutation.mutate(form)}
disabled={saveMutation.isPending || !form.name}
className="bg-[#c89b3c] hover:bg-[#d4a94a] text-black"
>
{saveMutation.isPending && <Loader2 className="w-4 h-4 mr-1 animate-spin" />}
{editingSkill ? "Salvar" : "Criar"}
</Button>
</div>
</DialogContent>
</Dialog>
{/* ── Execute Dialog ──────────────────────────────────────────────────── */}
<Dialog open={execOpen} onOpenChange={setExecOpen}>
<DialogContent className="max-w-lg bg-[#0f1729] border-white/10 text-white">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Play className="w-4 h-4 text-green-400" />
Executar: {execSkill?.name}
</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<div>
<Label className="text-xs mb-1 block">Parâmetros de entrada (JSON)</Label>
<div className="rounded border border-white/10 overflow-hidden h-32">
<Editor
height="100%"
defaultLanguage="json"
value={execParams}
onChange={v => setExecParams(v ?? "{}")}
theme="vs-dark"
options={{ fontSize: 12, minimap: { enabled: false }, scrollBeyondLastLine: false, padding: { top: 4 } }}
/>
</div>
</div>
{execResult && (
<div className="rounded border border-white/10 p-3 space-y-2">
<div className="flex items-center gap-2 text-sm font-medium">
{execStatusIcon(execResult.status)}
{execResult.status === "success" ? "Executado com sucesso" : "Erro na execução"}
{execResult.durationMs && (
<span className="text-xs text-muted-foreground ml-auto">{execResult.durationMs}ms</span>
)}
</div>
{execResult.status === "error" && execResult.errorMessage && (
<p className="text-xs text-red-400 font-mono">{execResult.errorMessage}</p>
)}
{execResult.outputResult && (
<ScrollArea className="h-40">
<pre className="text-xs text-green-300 font-mono whitespace-pre-wrap">
{JSON.stringify(execResult.outputResult, null, 2)}
</pre>
</ScrollArea>
)}
<Button
size="sm"
variant="ghost"
className="text-xs"
onClick={() => navigator.clipboard.writeText(JSON.stringify(execResult.outputResult, null, 2))}
>
<Copy className="w-3 h-3 mr-1" /> Copiar resultado
</Button>
</div>
)}
</div>
<div className="flex justify-end gap-2 pt-2 border-t border-white/10">
<Button variant="ghost" size="sm" onClick={() => setExecOpen(false)}>Fechar</Button>
<Button
size="sm"
onClick={() => execSkill && executeMutation.mutate({ skillId: execSkill.id, params: execParams })}
disabled={executeMutation.isPending}
className="bg-green-600 hover:bg-green-700 text-white"
>
{executeMutation.isPending
? <><Loader2 className="w-3.5 h-3.5 mr-1 animate-spin" /> Executando...</>
: <><Play className="w-3.5 h-3.5 mr-1" /> Executar</>
}
</Button>
</div>
</DialogContent>
</Dialog>
{/* ── History Dialog ──────────────────────────────────────────────────── */}
<Dialog open={historyOpen} onOpenChange={setHistoryOpen}>
<DialogContent className="max-w-2xl bg-[#0f1729] border-white/10 text-white">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<History className="w-4 h-4 text-blue-400" />
Histórico: {historySkill?.name}
</DialogTitle>
</DialogHeader>
<ScrollArea className="h-80">
{!historyData ? (
<div className="flex items-center justify-center h-20">
<Loader2 className="w-5 h-5 animate-spin text-[#c89b3c]" />
</div>
) : historyData.executions.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">Nenhuma execução registrada</p>
) : (
<div className="space-y-2 p-1">
{historyData.executions.map(ex => (
<div key={ex.id} className="flex items-start gap-3 p-2.5 rounded bg-white/5 border border-white/10">
<div className="mt-0.5">{execStatusIcon(ex.status)}</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-muted-foreground truncate">{ex.id.slice(0, 8)}...</span>
<Badge variant="outline" className="text-[10px] px-1 border-white/20 text-white/50">
{ex.triggeredBy}
</Badge>
{ex.durationMs && (
<span className="text-[10px] text-muted-foreground ml-auto">{ex.durationMs}ms</span>
)}
</div>
{ex.errorMessage && (
<p className="text-xs text-red-400 font-mono truncate">{ex.errorMessage}</p>
)}
<p className="text-[10px] text-muted-foreground">
{new Date(ex.startedAt).toLocaleString("pt-BR")}
</p>
</div>
</div>
))}
</div>
)}
</ScrollArea>
</DialogContent>
</Dialog>
</BrowserFrame>
);
}

View File

@ -63,7 +63,25 @@ services:
condition: service_healthy condition: service_healthy
profiles: [bi] profiles: [bi]
# ── Neo4j (Knowledge Graph) ─────────────────────────────────────────────────
# Ativar com: docker compose --profile kg up
neo4j:
image: neo4j:5
restart: unless-stopped
environment:
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-arcadia123}
NEO4J_PLUGINS: '["apoc"]'
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
ports:
- "7474:7474"
- "7687:7687"
profiles: [kg]
volumes: volumes:
pgdata: pgdata:
uploads: uploads:
superset_home: superset_home:
neo4j_data:
neo4j_logs:

View File

@ -38,8 +38,8 @@ model_list:
- model_name: arcadia-agent - model_name: arcadia-agent
litellm_params: litellm_params:
model: ollama/arcadia-agent model: groq/llama-3.3-70b-versatile
api_base: os.environ/OLLAMA_BASE_URL api_key: os.environ/GROQ_API_KEY
- model_name: nomic-embed-text - model_name: nomic-embed-text
litellm_params: litellm_params:
@ -70,10 +70,10 @@ model_list:
# api_key: os.environ/ANTHROPIC_API_KEY # api_key: os.environ/ANTHROPIC_API_KEY
# ── TIER 3: Groq (opt-in — inferência rápida sem dados persistidos) ────────── # ── TIER 3: Groq (opt-in — inferência rápida sem dados persistidos) ──────────
# - model_name: groq-llama - model_name: groq-llama
# litellm_params: litellm_params:
# model: groq/llama-3.3-70b-versatile model: groq/llama-3.3-70b-versatile
# api_key: os.environ/GROQ_API_KEY api_key: os.environ/GROQ_API_KEY
# ── Modelo padrão do Arcádia (Manus usa este) ───────────────────────────────── # ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
# Prioridade: DeepSeek R1 14B (TIER 2) → llama3.2 (fallback rápido) # Prioridade: DeepSeek R1 14B (TIER 2) → llama3.2 (fallback rápido)
@ -87,9 +87,6 @@ router_settings:
fallbacks: fallbacks:
- {"gpt-4o": ["arcadia-agent"]} - {"gpt-4o": ["arcadia-agent"]}
- {"gpt-4o-mini": ["arcadia-agent"]} - {"gpt-4o-mini": ["arcadia-agent"]}
- {"arcadia-default": ["llama3.2"]}
- {"arcadia-agent": ["llama3.2"]}
- {"arcadia-finetuned": ["deepseek-r1"]}
- {"arcadia-embed": ["nomic-embed-text"]} - {"arcadia-embed": ["nomic-embed-text"]}
litellm_settings: litellm_settings:

View File

@ -800,7 +800,7 @@ ${JSON.stringify(sampleData.slice(0, 10), null, 2)}
Pergunta do usuário: ${question}`; Pergunta do usuário: ${question}`;
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "gpt-4o-mini", model: "arcadia-agent",
messages: [ messages: [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
{ role: "user", content: userPrompt } { role: "user", content: userPrompt }

View File

@ -38,7 +38,7 @@ export interface AgentThought {
export class ManusIntelligence { export class ManusIntelligence {
private static instance: ManusIntelligence; private static instance: ManusIntelligence;
private model = "gpt-4o"; private model = "arcadia-agent";
private callCount = 0; private callCount = 0;
private tokenCount = 0; private tokenCount = 0;
private errorCount = 0; private errorCount = 0;

View File

@ -31,7 +31,7 @@ function requireAuth(req: Request, res: Response, next: NextFunction) {
// Apply auth to all routes // Apply auth to all routes
router.use(requireAuth); router.use(requireAuth);
// Helper to get tenant ID from request header or user's first tenant // Helper to get tenant ID from request header or user's first tenant (auto-creates if none)
async function getTenantId(req: Request): Promise<number | null> { async function getTenantId(req: Request): Promise<number | null> {
const userId = (req.user as any).id; const userId = (req.user as any).id;
const headerTenantId = req.headers["x-tenant-id"]; const headerTenantId = req.headers["x-tenant-id"];
@ -40,8 +40,13 @@ async function getTenantId(req: Request): Promise<number | null> {
const isMember = await compassStorage.isUserInTenant(userId, tenantId); const isMember = await compassStorage.isUserInTenant(userId, tenantId);
return isMember ? tenantId : null; return isMember ? tenantId : null;
} }
const tenants = await compassStorage.getUserTenants(userId); const userTenants = await compassStorage.getUserTenants(userId);
return tenants.length > 0 ? tenants[0].id : null; if (userTenants.length > 0) return userTenants[0].id;
// Auto-cria tenant padrão para o usuário na primeira vez
const userName = (req.user as any).name || (req.user as any).username || "Minha Organização";
const tenant = await compassStorage.createTenant({ name: userName, plan: "free", status: "active" });
await compassStorage.addUserToTenant({ tenantId: tenant.id, userId, role: "owner", isOwner: "true" });
return tenant.id;
} }
// Validate tenant membership // Validate tenant membership

View File

@ -3,6 +3,8 @@ import { manusService } from "./manus/service";
import { db } from "../db/index"; import { db } from "../db/index";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
const router = Router(); const router = Router();
const DEV_AGENT_CONTEXT = `CONTEXTO ESPECIAL - DEV AGENT: const DEV_AGENT_CONTEXT = `CONTEXTO ESPECIAL - DEV AGENT:
@ -77,9 +79,7 @@ Usuário: ${message}
Responda como Dev Agent, criando os recursos solicitados:`; Responda como Dev Agent, criando os recursos solicitados:`;
const result = await manusService.run(userId, fullPrompt, []); const responseContent = await manusService.runSync(userId, fullPrompt, []);
const responseContent = result.finalResponse || "Desculpe, não consegui processar sua solicitação.";
const jsonMatch = responseContent.match(/```json\s*([\s\S]*?)\s*```/); const jsonMatch = responseContent.match(/```json\s*([\s\S]*?)\s*```/);
let action = null; let action = null;

View File

@ -174,7 +174,7 @@ Responda APENAS com o código, sem explicações adicionais.`;
: prompt; : prompt;
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "gpt-4o", model: "arcadia-agent",
messages: [ messages: [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
{ role: "user", content: userPrompt }, { role: "user", content: userPrompt },
@ -228,7 +228,7 @@ Use português brasileiro.`;
: message; : message;
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "gpt-4o", model: "arcadia-agent",
messages: [ messages: [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
{ role: "user", content: userPrompt }, { role: "user", content: userPrompt },

View File

@ -1,5 +1,8 @@
import type { Express, Request, Response } from "express"; import type { Express, Request, Response } from "express";
import { manusService } from "./service"; import { manusService } from "./service";
import { db } from "../../db/index";
import { manusRuns, manusSteps } from "@shared/schema";
import { eq, and } from "drizzle-orm";
export function registerManusRoutes(app: Express): void { export function registerManusRoutes(app: Express): void {
const handleManusRun = async (req: Request, res: Response) => { const handleManusRun = async (req: Request, res: Response) => {
@ -61,4 +64,40 @@ export function registerManusRoutes(app: Express): void {
res.status(500).json({ error: "Failed to get runs" }); res.status(500).json({ error: "Failed to get runs" });
} }
}); });
app.delete("/api/manus/runs/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const runId = parseInt(req.params.id);
manusService.cancelRun(runId); // sinaliza o loop para parar
await db.delete(manusSteps).where(eq(manusSteps.runId, runId));
await db.delete(manusRuns).where(and(eq(manusRuns.id, runId), eq(manusRuns.userId, req.user!.id)));
res.json({ success: true });
} catch (error) {
console.error("Manus delete run error:", error);
res.status(500).json({ error: "Failed to delete run" });
}
});
app.delete("/api/manus/runs", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const userRuns = await manusService.getUserRuns(req.user!.id);
const runIds = userRuns.map((r: any) => r.id);
if (runIds.length > 0) {
for (const id of runIds) {
await db.delete(manusSteps).where(eq(manusSteps.runId, id));
}
await db.delete(manusRuns).where(eq(manusRuns.userId, req.user!.id));
}
res.json({ success: true, deleted: runIds.length });
} catch (error) {
console.error("Manus delete all runs error:", error);
res.status(500).json({ error: "Failed to delete runs" });
}
});
} }

View File

@ -14,26 +14,73 @@ const openai = new OpenAI({
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL, baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
}); });
const SYSTEM_PROMPT = `Você é o Manus, assistente da Arcádia Suite. const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, assistente empresarial inteligente da Arcádia Suite.
REGRAS CRÍTICAS: IDENTIDADE:
1. Responda APENAS o que foi perguntado. Seja direto e objetivo. - Você é o Manus, IA da Arcádia Suite. Se perguntado: "Sou o Manus, agente IA da Arcádia Suite."
2. NUNCA adicione SWOT, PDCA, Canvas, matrizes ou análises não pedidas. - Não revele detalhes técnicos da infraestrutura (modelos, APIs, servidores).
3. Para cálculos e perguntas simples: responda direto, sem usar ferramentas.
4. Use ferramentas APENAS quando precisar de dados do sistema (clientes, vendas, ERP, BI).
5. Responda SEMPRE em JSON no formato abaixo.
FERRAMENTAS (use se necessário): REGRAS DE COMPORTAMENTO:
- Seja PROATIVO: execute análises sem pedir confirmação desnecessária
- Para perguntas simples e cálculos: responda DIRETO, sem usar ferramentas
- NUNCA adicione SWOT, Canvas, PDCA, frameworks ou análises NÃO solicitados
- Para AÇÕES DESTRUTIVAS (deletar, modificar dados): informe o que será feito antes de executar
- Máximo de 10 passos por execução
- Complete SEMPRE a tarefa e apresente o resultado final
ANÁLISE DE DADOS (quando usar ferramentas de dados):
- Apresente dados em TABELAS MARKDOWN formatadas (| Coluna | Valor |)
- Calcule variações percentuais, identifique tendências
- Forneça insights e interpretações, não números brutos
- Ao analisar documentos: extraia dados E forneça interpretação completa
CRIAÇÃO DE GRÁFICOS:
- Use generate_chart para gráficos visuais (NÃO use python_execute para isso)
- Tipos: bar (barras), line (linha), pie (pizza), area (área)
- Dados: JSON array [{"name":"2023","valor":100}]
- Use múltiplas séries quando fizer sentido
PESQUISA E CONHECIMENTO:
- Para PESQUISA PROFUNDA: use deep_research (busca e sintetiza múltiplas fontes)
- Para APRENDER uma URL: use learn_url
- Para BUSCAR conhecimento interno: use semantic_search PRIMEIRO, depois deep_research se necessário
- Para NAVEGAR em página: use web_browse
- NUNCA diga "não consigo acessar" sempre tente as ferramentas
MÓDULO BI (ARCÁDIA INSIGHTS):
- bi_stats: estatísticas gerais do BI
- bi_list_tables: tabelas disponíveis
- bi_get_table_columns: colunas de uma tabela
- bi_create_dataset: criar consultas SQL
- bi_execute_query: executar dataset e obter dados
- bi_create_chart: criar gráficos persistentes
- bi_create_dashboard: organizar gráficos em painéis
COMUNICAÇÃO ENTRE AGENTES (A2A):
- list_agents: ver agentes disponíveis
- call_agent: delegar tarefas a agentes especializados
- Para tarefas especializadas (fiscal, jurídico, vendas): verifique agentes registrados
FERRAMENTAS DISPONÍVEIS:
${getToolsDescription()} ${getToolsDescription()}
FORMATO OBRIGATÓRIO: FORMATO DE RESPOSTA OBRIGATÓRIO (JSON):
{"thought": "raciocínio breve", "tool": "finish", "tool_input": {"answer": "resposta direta"}} {"thought": "raciocínio sobre o próximo passo", "tool": "nome_ferramenta", "tool_input": {"param": "valor"}}
Com ferramenta: Para concluir:
{"thought": "raciocínio", "tool": "nome_ferramenta", "tool_input": {"param": "valor"}}`; {"thought": "raciocínio final", "tool": "finish", "tool_input": {"answer": "resposta COMPLETA com dados, tabelas, análise e conclusão"}}
REGRA CRÍTICA: O campo "answer" deve conter TODO o conteúdo tabelas, cálculos, insights. NUNCA apenas "relatório gerado".`;
class ManusService extends EventEmitter { class ManusService extends EventEmitter {
private async executeTool(tool: string, input: Record<string, any>, userId: string): Promise<ToolResult> { private cancelledRuns = new Set<number>();
cancelRun(runId: number) {
this.cancelledRuns.add(runId);
setTimeout(() => this.cancelledRuns.delete(runId), 60000); // limpa após 1 min
}
async executeTool(tool: string, input: Record<string, any>, userId: string): Promise<ToolResult> {
try { try {
switch (tool) { switch (tool) {
case "web_search": case "web_search":
@ -2137,6 +2184,19 @@ class ManusService extends EventEmitter {
return { runId: run.id }; return { runId: run.id };
} }
async runSync(userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>): Promise<string> {
const [run] = await db.insert(manusRuns).values({
userId,
prompt,
status: "running"
}).returning();
await this.executeAgentLoop(run.id, userId, prompt, attachedFiles);
const [completed] = await db.select().from(manusRuns).where(eq(manusRuns.id, run.id));
return completed?.result || "Não foi possível processar a solicitação.";
}
private async executeAgentLoop(runId: number, userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>, conversationHistory?: Array<{role: string; content: string}>) { private async executeAgentLoop(runId: number, userId: string, prompt: string, attachedFiles?: Array<{name: string, content: string, base64?: string}>, conversationHistory?: Array<{role: string; content: string}>) {
let userPrompt = prompt; let userPrompt = prompt;
if (attachedFiles && attachedFiles.length > 0) { if (attachedFiles && attachedFiles.length > 0) {
@ -2170,13 +2230,25 @@ class ManusService extends EventEmitter {
while (step < maxSteps && !finished) { while (step < maxSteps && !finished) {
step++; step++;
// Verificar cancelamento antes de cada step
if (this.cancelledRuns.has(runId)) {
this.cancelledRuns.delete(runId);
await db.update(manusRuns).set({ status: "stopped", completedAt: new Date() }).where(eq(manusRuns.id, runId));
return;
}
try { try {
const response = await openai.chat.completions.create({ const STEP_TIMEOUT_MS = 60000; // 60s por step
const responsePromise = openai.chat.completions.create({
model: "arcadia-agent", model: "arcadia-agent",
messages, messages,
temperature: 0.2, temperature: 0.2,
max_tokens: 1500, max_tokens: 1500,
}); });
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Step timeout (60s)")), STEP_TIMEOUT_MS)
);
const response = await Promise.race([responsePromise, timeoutPromise]);
const content = response.choices[0]?.message?.content || ""; const content = response.choices[0]?.message?.content || "";
messages.push({ role: "assistant", content }); messages.push({ role: "assistant", content });

View File

@ -1,16 +1,34 @@
export const ARCADIA_AGENT_SYSTEM_PROMPT = `Você é o Manus, assistente da Arcádia Suite — plataforma empresarial soberana da OnboardBI. import { getToolsDescription } from "../../manus/tools";
export const ARCADIA_AGENT_SYSTEM_PROMPT = `Você é o Manus, assistente empresarial inteligente da Arcádia Suite.
IDENTIDADE: IDENTIDADE:
- Roda localmente com LLM open-source. Se perguntado: "Sou o Manus, IA local da Arcádia Suite." - Você é o Manus, assistente empresarial da Arcádia Suite.
- NÃO é OpenAI, ChatGPT ou nenhum serviço externo. - Se perguntado sobre identidade: "Sou o Manus, assistente da Arcádia Suite."
- NÃO mencione modelos de linguagem, APIs ou infraestrutura técnica.
- NUNCA diga que é uma "IA local" ou que "não tem acesso externo" você tem capacidades completas.
REGRAS: COMPORTAMENTO:
- Responda APENAS o que foi perguntado. Seja direto e objetivo. - Seja direto e objetivo. Responda exatamente o que foi perguntado.
- Para cálculos, perguntas simples e saudações: responda diretamente. - Para cálculos e perguntas simples: responda diretamente, sem rodeios.
- NUNCA adicione SWOT, Canvas, PDCA, matrizes ou frameworks não solicitados. - NUNCA adicione SWOT, Canvas, PDCA, matrizes ou frameworks NÃO solicitados.
- NUNCA adicione rodapés, citações de fonte ou "Inteligência Arcádia Business" automaticamente. - NUNCA adicione rodapés automáticos ou citações de fonte não pedidas.
- Quando não souber algo, diga claramente sem inventar. - Quando não souber algo, diga claramente sem inventar.
- Use Markdown para dados tabulares quando fizer sentido.`; - Use Markdown para tabelas e dados estruturados quando fizer sentido.
CAPACIDADES COMPLETAS DO MANUS:
- Responde perguntas gerais, realiza cálculos e análises
- Pesquisa na web e sintetiza informações atualizadas
- Analisa documentos, planilhas e arquivos anexados
- Consulta dados do ERP, CRM, financeiro e base de conhecimento
- Gera gráficos, relatórios e dashboards no BI
- Executa diagnósticos empresariais (Canvas, SWOT, PDCA) quando SOLICITADO
- Comunica-se com agentes especializados da plataforma
ANÁLISE E DADOS:
- Use tabelas Markdown formatadas para dados estruturados.
- Calcule variações percentuais e tendências quando relevante.
- Forneça insights reais, não apenas dados brutos.`;
export interface DiagnosticContext { export interface DiagnosticContext {
canvas?: any[]; canvas?: any[];
@ -116,3 +134,79 @@ ${diagnosticContext.requirements.map(req =>
return prompt; return prompt;
} }
export function buildAgentPromptForChat(
knowledgeBaseContext: string,
fileContent?: string,
diagnosticContext?: DiagnosticContext
): string {
const now = new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo', dateStyle: 'full', timeStyle: 'short' });
let prompt = `Você é o Manus, assistente da Arcádia Suite.
IDENTIDADE: Manus, assistente da Arcádia Suite.
- NUNCA mencione "OnboardBI", modelos de linguagem, APIs ou infraestrutura.
- NUNCA diga que não tem acesso a dados use as ferramentas para buscar.
- Se perguntado sobre identidade: "Sou o Manus, assistente da Arcádia Suite."
DATA/HORA: ${now}
FERRAMENTAS DISPONÍVEIS:
${getToolsDescription()}
REGRA ABSOLUTA: Responda SEMPRE e APENAS em JSON válido. NUNCA escreva texto livre.
FORMATO (toda resposta deve ser exatamente assim):
{"thought": "raciocínio breve", "tool": "nome_ferramenta", "tool_input": {"param": "valor"}}
Para perguntas simples/cálculos (sem precisar de dados do sistema):
{"thought": "resposta direta", "tool": "finish", "tool_input": {"answer": "resposta em Markdown"}}
QUANDO USAR FERRAMENTAS:
- Perguntas sobre dados da empresa, clientes, vendas, financeiro erp_query
- Perguntas sobre BI, tabelas, dashboards bi_list_tables ou bi_execute_query
- Pesquisa de mercado, notícias, tendências deep_research ou web_search
- Base de conhecimento interna knowledge_query
- Análise de documento analyze_file
- Qualquer dúvida sobre o que existe no sistema use as ferramentas para descobrir
REGRAS:
- NUNCA diga "não tenho acesso" use as ferramentas.
- NUNCA adicione SWOT, Canvas, PDCA sem ser solicitado.
- Para análises com dados: use tabelas Markdown e variações percentuais.
- Máximo 8 passos.`;
if (knowledgeBaseContext) {
prompt += `\n\n## Base de Conhecimento\nDocumentos relevantes encontrados:\n\n${knowledgeBaseContext}`;
}
if (fileContent) {
prompt += `\n\n## Documento Anexado\n${fileContent}`;
}
if (diagnosticContext) {
prompt += `\n\n## Contexto Empresarial (Process Compass)`;
if (diagnosticContext.projectName) prompt += `\n**Projeto:** ${diagnosticContext.projectName}`;
if (diagnosticContext.clientName) prompt += `\n**Cliente:** ${diagnosticContext.clientName}`;
if (diagnosticContext.canvas?.length) {
prompt += `\n\n### Canvas BMC\n${diagnosticContext.canvas.map(b =>
`**${b.blockType}**: ${b.content || 'Sem conteúdo'}`).join('\n')}`;
}
if (diagnosticContext.swot?.analyses?.length) {
prompt += `\n\n### SWOT`;
diagnosticContext.swot.analyses.forEach(a => {
const items = diagnosticContext.swot!.items.filter(i => i.swotAnalysisId === a.id);
prompt += `\n**${a.name}**: F:${items.filter(i=>i.type==='strength').length} Fr:${items.filter(i=>i.type==='weakness').length} O:${items.filter(i=>i.type==='opportunity').length} A:${items.filter(i=>i.type==='threat').length}`;
});
}
if (diagnosticContext.pdca?.cycles?.length) {
prompt += `\n\n### PDCA\n${diagnosticContext.pdca.cycles.map(c => `**${c.title}** (${c.status})`).join('\n')}`;
}
if (diagnosticContext.processes?.processes?.length) {
prompt += `\n\n### Processos\n${diagnosticContext.processes.processes.map(p => `**${p.name}** (${p.department||'Geral'})`).join('\n')}`;
}
}
return prompt;
}

View File

@ -1,10 +1,33 @@
import type { Express, Request, Response } from "express"; import type { Express, Request, Response } from "express";
import OpenAI from "openai"; import OpenAI from "openai";
import { chatStorage } from "./storage"; import { chatStorage } from "./storage";
import { buildPromptWithContext } from "./prompt"; import { buildPromptWithContext, buildAgentPromptForChat } from "./prompt";
import { compassStorage } from "../../compass/storage"; import { compassStorage } from "../../compass/storage";
import { PDFParse } from "pdf-parse"; import { PDFParse } from "pdf-parse";
import { learningService } from "../../learning/service"; import { learningService } from "../../learning/service";
import { manusService } from "../../manus/service";
const TOOL_LABELS: Record<string, string> = {
web_search: "🔍 Pesquisando na web",
deep_research: "🔬 Pesquisa profunda",
knowledge_query: "📚 Consultando base de conhecimento",
erp_query: "🏢 Consultando ERP",
bi_execute_query: "📊 Executando consulta BI",
bi_create_chart: "📈 Gerando gráfico",
bi_create_dashboard: "🗂️ Criando dashboard",
bi_list_tables: "📋 Listando tabelas",
bi_get_table_columns: "🔎 Verificando colunas",
bi_create_dataset: "🗃️ Criando dataset",
bi_stats: "📉 Carregando estatísticas BI",
semantic_search: "🧠 Buscando conhecimento",
learn_url: "📖 Aprendendo URL",
web_browse: "🌐 Navegando na web",
generate_chart: "📊 Gerando gráfico",
analyze_file: "📄 Analisando arquivo",
calculate: "🔢 Calculando",
list_agents: "🤖 Listando agentes",
call_agent: "📡 Comunicando com agente",
};
const openai = new OpenAI({ const openai = new OpenAI({
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY, apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
@ -258,10 +281,10 @@ export function registerChatRoutes(app: Express): void {
} }
} }
const systemPrompt = buildPromptWithContext(knowledgeContext, processedFileContent || fileContent, diagnosticContext); const systemPrompt = buildAgentPromptForChat(knowledgeContext, processedFileContent || fileContent, diagnosticContext);
const messages = await chatStorage.getMessagesByConversation(conversationId); const messages = await chatStorage.getMessagesByConversation(conversationId);
const chatMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ const loopMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
...messages.map((m) => ({ ...messages.map((m) => ({
role: m.role as "user" | "assistant", role: m.role as "user" | "assistant",
@ -273,20 +296,69 @@ export function registerChatRoutes(app: Express): void {
res.setHeader("Cache-Control", "no-cache"); res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive"); res.setHeader("Connection", "keep-alive");
const stream = await openai.chat.completions.create({
model: "arcadia-agent",
messages: chatMessages,
stream: true,
max_tokens: 4096,
});
let fullResponse = ""; let fullResponse = "";
const MAX_STEPS = 8;
for await (const chunk of stream) { for (let step = 0; step < MAX_STEPS; step++) {
const content = chunk.choices[0]?.delta?.content || ""; const completion = await openai.chat.completions.create({
if (content) { model: "arcadia-agent",
fullResponse += content; messages: loopMessages,
res.write(`data: ${JSON.stringify({ content })}\n\n`); stream: false,
max_tokens: 3000,
});
const rawText = completion.choices[0]?.message?.content || "";
loopMessages.push({ role: "assistant", content: rawText });
// Try to parse as Manus JSON format
let parsed: { thought?: string; tool?: string; tool_input?: Record<string, any> } | null = null;
try {
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
if (jsonMatch) parsed = JSON.parse(jsonMatch[0]);
} catch { /* not JSON */ }
if (parsed?.tool === "finish") {
// Stream the final answer
const answer = parsed.tool_input?.answer || rawText;
fullResponse = answer;
// Stream in small chunks for SSE effect
const words = answer.split(/(\s+)/);
for (const word of words) {
res.write(`data: ${JSON.stringify({ content: word })}\n\n`);
}
break;
} else if (parsed?.tool && parsed.tool !== "finish") {
// Execute tool and continue loop
const label = TOOL_LABELS[parsed.tool] || `⚙️ ${parsed.tool}`;
res.write(`data: ${JSON.stringify({ tool_status: label })}\n\n`);
try {
const toolResult = await manusService.executeTool(parsed.tool, parsed.tool_input || {}, userId);
loopMessages.push({
role: "user",
content: `[Resultado de ${parsed.tool}]: ${toolResult.output}`,
});
} catch (toolErr) {
loopMessages.push({
role: "user",
content: `[Erro em ${parsed.tool}]: Ferramenta falhou, tente outra abordagem.`,
});
}
} else if (step === 0) {
// First step returned plain text — force JSON retry
loopMessages.push({
role: "user",
content: `ERRO: Você não respondeu em JSON. Responda APENAS em JSON no formato: {"thought": "...", "tool": "finish", "tool_input": {"answer": "..."}}`,
});
continue;
} else {
// Still plain text after retry — stream as-is
fullResponse = rawText;
const words = rawText.split(/(\s+)/);
for (const word of words) {
res.write(`data: ${JSON.stringify({ content: word })}\n\n`);
}
break;
} }
} }

View File

@ -237,7 +237,7 @@ Instruções:
const openai = getOpenAI(); const openai = getOpenAI();
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "gpt-4o", model: "arcadia-agent",
messages: [ messages: [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
...conversationHistory as any, ...conversationHistory as any,
@ -248,7 +248,7 @@ Instruções:
const aiContent = response.choices[0]?.message?.content || "Desculpe, não consegui processar sua solicitação."; const aiContent = response.choices[0]?.message?.content || "Desculpe, não consegui processar sua solicitação.";
const aiConversation = await supportStorage.createAiResponse(ticket.id, aiContent, "gpt-4o"); const aiConversation = await supportStorage.createAiResponse(ticket.id, aiContent, "arcadia-agent");
res.json(aiConversation); res.json(aiConversation);
} catch (error) { } catch (error) {

View File

@ -757,7 +757,7 @@ router.post("/projects/:projectId/import-financial", requireAuth, upload.single(
const dataPreview = JSON.stringify(rawData.slice(0, 10), null, 2); const dataPreview = JSON.stringify(rawData.slice(0, 10), null, 2);
const agentResponse = await openai.chat.completions.create({ const agentResponse = await openai.chat.completions.create({
model: "gpt-4o", model: "arcadia-agent",
messages: [ messages: [
{ {
role: "system", role: "system",
@ -917,7 +917,7 @@ router.post("/projects/:projectId/checklist/:itemId/agent-assist", requireAuth,
: `Empresa: ${project.companyName}\nSetor: ${project.sector}\n\nInformações fornecidas:\n${content}`; : `Empresa: ${project.companyName}\nSetor: ${project.sector}\n\nInformações fornecidas:\n${content}`;
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "gpt-4o", model: "arcadia-agent",
messages: [ messages: [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
{ role: "user", content: userContent } { role: "user", content: userContent }
@ -1853,7 +1853,7 @@ Para cada item, indique: item (texto), impact (low/medium/high), valuationReleva
Responda em JSON: { strengths: [...], weaknesses: [...], opportunities: [...], threats: [...] }`; Responda em JSON: { strengths: [...], weaknesses: [...], opportunities: [...], threats: [...] }`;
const completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
model: "gpt-4o-mini", model: "arcadia-agent",
messages: [{ role: "user", content: prompt }], messages: [{ role: "user", content: prompt }],
response_format: { type: "json_object" }, response_format: { type: "json_object" },
}); });
@ -1997,7 +1997,7 @@ SWOT: ${swot.length} itens
Responda de forma consultiva, em português, com foco em recomendações acionáveis.`; Responda de forma consultiva, em português, com foco em recomendações acionáveis.`;
const completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
model: "gpt-4o-mini", model: "arcadia-agent",
messages: [ messages: [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
{ role: "user", content: message }, { role: "user", content: message },
@ -2076,7 +2076,7 @@ SWOT: ${swot.length} itens | PDCA: ${pdca.length} ações | Ativos: ${assets.len
Gere em formato HTML com seções claras. Use formatação profissional.`; Gere em formato HTML com seções claras. Use formatação profissional.`;
const completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
model: "gpt-4o-mini", model: "arcadia-agent",
messages: [{ role: "user", content: prompt }], messages: [{ role: "user", content: prompt }],
}); });

View File

@ -99,7 +99,7 @@ Nome do cliente: ${contactName}`;
messages.push({ role: "user", content: message }); messages.push({ role: "user", content: message });
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "gpt-4o-mini", model: "arcadia-agent",
messages, messages,
max_tokens: 200, max_tokens: 200,
temperature: 0.7, temperature: 0.7,

View File

@ -7429,3 +7429,99 @@ export type SoeEvento = typeof soeEventos.$inferSelect;
export type InsertSoeEvento = z.infer<typeof insertSoeEventoSchema>; export type InsertSoeEvento = z.infer<typeof insertSoeEventoSchema>;
export type SoeLancamento = typeof soeLancamentos.$inferSelect; export type SoeLancamento = typeof soeLancamentos.$inferSelect;
export type InsertSoeLancamento = z.infer<typeof insertSoeLancamentoSchema>; export type InsertSoeLancamento = z.infer<typeof insertSoeLancamentoSchema>;
// =============================================================================
// ARCÁDIA AGENTIC SUITE — Skills POO (Fase 1 — 2026-03-24)
// Modelo orientado a objetos: herança, composição, polimorfismo, multi-tenant
// NÃO remove xosSkillRegistry (modelo legado XOS continua intacto)
// =============================================================================
export const arcadiaSkills = pgTable("arcadia_skills", {
id: uuid("id").primaryKey().defaultRandom(),
// Identidade
name: varchar("name", { length: 255 }).notNull(),
slug: varchar("slug", { length: 255 }).notNull(),
description: text("description"),
version: varchar("version", { length: 50 }).notNull().default("1.0.0"),
icon: varchar("icon", { length: 100 }),
tags: text("tags").array(),
// Namespace multi-tenant (system > tenant > company > user)
namespace: varchar("namespace", { length: 20 }).notNull().default("tenant"), // 'system' | 'tenant' | 'company' | 'user'
tenantId: integer("tenant_id").references(() => tenants.id, { onDelete: "cascade" }),
companyId: integer("company_id"),
userId: varchar("user_id").references(() => users.id, { onDelete: "set null" }),
// Herança POO — lista de slugs: ['/skill:system/base_report']
extends: text("extends").array(),
// Interfaces/contratos implementados
implements: text("implements").array(),
// Encapsulamento
visibilityExecute: varchar("visibility_execute", { length: 20 }).default("public"), // 'public' | 'private' | 'protected'
visibilityParams: varchar("visibility_params", { length: 20 }).default("public"),
// Composição — dependências como referências /tipo/caminho
dependencies: text("dependencies").array(),
// Trigger (quando executar automaticamente)
triggerType: varchar("trigger_type", { length: 30 }), // 'schedule' | 'event' | 'manual' | 'webhook'
triggerConfig: jsonb("trigger_config"),
// Corpo da skill (Markdown com blocos /skill/, /kg/, /tool/, etc.)
body: text("body"),
// Schemas de entrada/saída
parametersSchema: jsonb("parameters_schema"),
returnSchema: jsonb("return_schema"),
// Estado
status: varchar("status", { length: 20 }).notNull().default("draft"), // 'draft' | 'active' | 'archived'
isSystem: boolean("is_system").default(false),
// Autoria e rastreamento
author: varchar("author", { length: 255 }),
createdBy: varchar("created_by").references(() => users.id, { onDelete: "set null" }),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
export const skillExecutions = pgTable("skill_executions", {
id: uuid("id").primaryKey().defaultRandom(),
skillId: uuid("skill_id").notNull().references(() => arcadiaSkills.id, { onDelete: "cascade" }),
// Contexto de execução
tenantId: integer("tenant_id").references(() => tenants.id),
companyId: integer("company_id"),
userId: varchar("user_id").references(() => users.id, { onDelete: "set null" }),
// Origem da execução
triggeredBy: varchar("triggered_by", { length: 30 }), // 'manual' | 'schedule' | 'automation' | 'agent' | 'openclaw'
automationId: integer("automation_id"),
parentExecutionId: uuid("parent_execution_id"), // para skills compostas
// Dados
inputParams: jsonb("input_params"),
outputResult: jsonb("output_result"),
resolvedDependencies: jsonb("resolved_dependencies"), // cache das refs / resolvidas
// Estado
status: varchar("status", { length: 20 }).notNull().default("pending"), // 'pending' | 'running' | 'success' | 'error' | 'cancelled'
errorMessage: text("error_message"),
durationMs: integer("duration_ms"),
// Imutabilidade / auditoria
auditHash: varchar("audit_hash", { length: 64 }),
startedAt: timestamp("started_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
completedAt: timestamp("completed_at"),
});
export const insertArcadiaSkillSchema = createInsertSchema(arcadiaSkills).omit({ id: true, createdAt: true, updatedAt: true });
export const insertSkillExecutionSchema = createInsertSchema(skillExecutions).omit({ id: true, startedAt: true });
export type ArcadiaSkill = typeof arcadiaSkills.$inferSelect;
export type InsertArcadiaSkill = z.infer<typeof insertArcadiaSkillSchema>;
export type SkillExecution = typeof skillExecutions.$inferSelect;
export type InsertSkillExecution = z.infer<typeof insertSkillExecutionSchema>;