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 XosReports = lazy(() => import("@/pages/XosReports"));
const XosProtocols = lazy(() => import("@/pages/XosProtocols"));
const Skills = lazy(() => import("@/pages/Skills"));
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" });
}
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 }> {
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
</Button>
</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>
{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>
<ScrollArea className="flex-1 px-2">
{loadingRuns ? (
@ -1498,22 +1525,35 @@ export default function Agent() {
) : (
<div className="space-y-1 pb-2">
{manusRuns.map((run) => (
<button
key={run.id}
onClick={() => setSelectedRun(run.id)}
className={`w-full text-left p-2.5 rounded-lg transition-all ${
selectedRun === run.id
? "bg-[#c89b3c]/20 border border-[#c89b3c]/50"
: "hover:bg-[#162638] border border-transparent"
}`}
data-testid={`run-${run.id}`}
>
<p className="text-xs text-white line-clamp-2 mb-1.5">{run.prompt}</p>
<div className="flex items-center gap-2">
{getStatusBadge(run.status)}
<span className="text-[10px] text-white/40">{formatTime(run.createdAt)}</span>
</div>
</button>
<div key={run.id} className="group relative">
<button
onClick={() => setSelectedRun(run.id)}
className={`w-full text-left p-2.5 rounded-lg transition-all pr-8 ${
selectedRun === run.id
? "bg-[#c89b3c]/20 border border-[#c89b3c]/50"
: "hover:bg-[#162638] border border-transparent"
}`}
data-testid={`run-${run.id}`}
>
<p className="text-xs text-white line-clamp-2 mb-1.5">{run.prompt}</p>
<div className="flex items-center gap-2">
{getStatusBadge(run.status)}
<span className="text-[10px] text-white/40">{formatTime(run.createdAt)}</span>
</div>
</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>
)}

View File

@ -1855,7 +1855,7 @@ export default function ProcessCompass() {
data-testid="search-projects"
/>
</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
</Button>
</div>
@ -4600,18 +4600,32 @@ export default function ProcessCompass() {
</div>
<div className="space-y-2">
<Label htmlFor="project-client">Cliente</Label>
<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>
{clients.length === 0 ? (
<div className="rounded-md border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-800">
Nenhum cliente cadastrado.{" "}
<button
type="button"
className="underline font-medium"
onClick={() => { setShowNewProjectDialog(false); setShowNewClientDialog(true); }}
>
Criar cliente
</button>{" "}
antes de criar um projeto.
</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 className="space-y-2">
<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
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:
pgdata:
uploads:
superset_home:
neo4j_data:
neo4j_logs:

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ function requireAuth(req: Request, res: Response, next: NextFunction) {
// Apply auth to all routes
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> {
const userId = (req.user as any).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);
return isMember ? tenantId : null;
}
const tenants = await compassStorage.getUserTenants(userId);
return tenants.length > 0 ? tenants[0].id : null;
const userTenants = await compassStorage.getUserTenants(userId);
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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
import type { Express, Request, Response } from "express";
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 {
const handleManusRun = async (req: Request, res: Response) => {
@ -53,7 +56,7 @@ export function registerManusRoutes(app: Express): void {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const runs = await manusService.getUserRuns(req.user!.id);
res.json(runs);
} catch (error) {
@ -61,4 +64,40 @@ export function registerManusRoutes(app: Express): void {
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,
});
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:
1. Responda APENAS o que foi perguntado. Seja direto e objetivo.
2. NUNCA adicione SWOT, PDCA, Canvas, matrizes ou análises não pedidas.
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.
IDENTIDADE:
- Você é o Manus, IA da Arcádia Suite. Se perguntado: "Sou o Manus, agente IA da Arcádia Suite."
- Não revele detalhes técnicos da infraestrutura (modelos, APIs, servidores).
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()}
FORMATO OBRIGATÓRIO:
{"thought": "raciocínio breve", "tool": "finish", "tool_input": {"answer": "resposta direta"}}
FORMATO DE RESPOSTA OBRIGATÓRIO (JSON):
{"thought": "raciocínio sobre o próximo passo", "tool": "nome_ferramenta", "tool_input": {"param": "valor"}}
Com ferramenta:
{"thought": "raciocínio", "tool": "nome_ferramenta", "tool_input": {"param": "valor"}}`;
Para concluir:
{"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 {
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 {
switch (tool) {
case "web_search":
@ -2137,6 +2184,19 @@ class ManusService extends EventEmitter {
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}>) {
let userPrompt = prompt;
if (attachedFiles && attachedFiles.length > 0) {
@ -2169,14 +2229,26 @@ class ManusService extends EventEmitter {
while (step < maxSteps && !finished) {
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 {
const response = await openai.chat.completions.create({
const STEP_TIMEOUT_MS = 60000; // 60s por step
const responsePromise = openai.chat.completions.create({
model: "arcadia-agent",
messages,
temperature: 0.2,
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 || "";
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:
- Roda localmente com LLM open-source. Se perguntado: "Sou o Manus, IA local da Arcádia Suite."
- NÃO é OpenAI, ChatGPT ou nenhum serviço externo.
- Você é o Manus, assistente empresarial da Arcádia Suite.
- 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:
- Responda APENAS o que foi perguntado. Seja direto e objetivo.
- Para cálculos, perguntas simples e saudações: responda diretamente.
- 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.
COMPORTAMENTO:
- Seja direto e objetivo. Responda exatamente o que foi perguntado.
- Para cálculos e perguntas simples: responda diretamente, sem rodeios.
- NUNCA adicione SWOT, Canvas, PDCA, matrizes ou frameworks NÃO solicitados.
- NUNCA adicione rodapés automáticos ou citações de fonte não pedidas.
- 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 {
canvas?: any[];
@ -116,3 +134,79 @@ ${diagnosticContext.requirements.map(req =>
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 OpenAI from "openai";
import { chatStorage } from "./storage";
import { buildPromptWithContext } from "./prompt";
import { buildPromptWithContext, buildAgentPromptForChat } from "./prompt";
import { compassStorage } from "../../compass/storage";
import { PDFParse } from "pdf-parse";
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({
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 chatMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
const loopMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...messages.map((m) => ({
role: m.role as "user" | "assistant",
@ -273,20 +296,69 @@ export function registerChatRoutes(app: Express): void {
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const stream = await openai.chat.completions.create({
model: "arcadia-agent",
messages: chatMessages,
stream: true,
max_tokens: 4096,
});
let fullResponse = "";
const MAX_STEPS = 8;
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || "";
if (content) {
fullResponse += content;
res.write(`data: ${JSON.stringify({ content })}\n\n`);
for (let step = 0; step < MAX_STEPS; step++) {
const completion = await openai.chat.completions.create({
model: "arcadia-agent",
messages: loopMessages,
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 response = await openai.chat.completions.create({
model: "gpt-4o",
model: "arcadia-agent",
messages: [
{ role: "system", content: systemPrompt },
...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 aiConversation = await supportStorage.createAiResponse(ticket.id, aiContent, "gpt-4o");
const aiConversation = await supportStorage.createAiResponse(ticket.id, aiContent, "arcadia-agent");
res.json(aiConversation);
} 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 agentResponse = await openai.chat.completions.create({
model: "gpt-4o",
model: "arcadia-agent",
messages: [
{
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}`;
const response = await openai.chat.completions.create({
model: "gpt-4o",
model: "arcadia-agent",
messages: [
{ role: "system", content: systemPrompt },
{ 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: [...] }`;
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
model: "arcadia-agent",
messages: [{ role: "user", content: prompt }],
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.`;
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
model: "arcadia-agent",
messages: [
{ role: "system", content: systemPrompt },
{ 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.`;
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
model: "arcadia-agent",
messages: [{ role: "user", content: prompt }],
});

View File

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

View File

@ -7429,3 +7429,99 @@ export type SoeEvento = typeof soeEventos.$inferSelect;
export type InsertSoeEvento = z.infer<typeof insertSoeEventoSchema>;
export type SoeLancamento = typeof soeLancamentos.$inferSelect;
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>;