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:
parent
3c61acf4d9
commit
c9a0563422
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
@ -53,7 +56,7 @@ export function registerManusRoutes(app: Express): void {
|
||||||
if (!req.isAuthenticated()) {
|
if (!req.isAuthenticated()) {
|
||||||
return res.status(401).json({ error: "Not authenticated" });
|
return res.status(401).json({ error: "Not authenticated" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const runs = await manusService.getUserRuns(req.user!.id);
|
const runs = await manusService.getUserRuns(req.user!.id);
|
||||||
res.json(runs);
|
res.json(runs);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 só 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 só 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) {
|
||||||
|
|
@ -2169,14 +2229,26 @@ 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 });
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue