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 XosReports = lazy(() => import("@/pages/XosReports"));
|
||||
const XosProtocols = lazy(() => import("@/pages/XosProtocols"));
|
||||
const Skills = lazy(() => import("@/pages/Skills"));
|
||||
|
||||
|
||||
function LoadingFallback() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
@ -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" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 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()}
|
||||
|
||||
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) {
|
||||
|
|
@ -2170,13 +2230,25 @@ 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 });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 }],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Reference in New Issue