chore: rename service folders to lowercase

This commit is contained in:
Ruslan Bakiev
2026-02-20 12:10:25 +07:00
parent 0fdf5cf021
commit 46cca064df
71 changed files with 10 additions and 10 deletions

View File

@@ -0,0 +1,270 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { ChatRole, Prisma } from "@prisma/client";
import { prisma } from "../utils/prisma";
import { datasetRoot } from "../dataset/paths";
import { ensureDataset } from "../dataset/exporter";
import { runLangGraphCrmAgentFor } from "./langgraphCrmAgent";
import type { ChangeSet } from "../utils/changeSet";
type ContactIndexRow = {
id: string;
name: string;
company: string | null;
lastMessageAt: string | null;
nextEventAt: string | null;
updatedAt: string;
};
export type AgentReply = {
text: string;
plan: string[];
tools: string[];
thinking?: string[];
toolRuns?: Array<{
name: string;
status: "ok" | "error";
input: string;
output: string;
at: string;
}>;
dbWrites?: Array<{ kind: string; detail: string }>;
};
export type AgentTraceEvent = {
text: string;
toolRun?: {
name: string;
status: "ok" | "error";
input: string;
output: string;
at: string;
};
};
function normalize(s: string) {
return s.trim().toLowerCase();
}
function isToday(date: Date) {
const now = new Date();
return (
date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate()
);
}
async function readContactIndex(): Promise<ContactIndexRow[]> {
throw new Error("readContactIndex now requires dataset root");
}
async function readContactIndexFrom(root: string): Promise<ContactIndexRow[]> {
const p = path.join(root, "index", "contacts.json");
const raw = await fs.readFile(p, "utf8");
return JSON.parse(raw);
}
async function countJsonlLines(p: string): Promise<number> {
const raw = await fs.readFile(p, "utf8");
if (!raw.trim()) return 0;
// cheap line count (JSONL is 1 item per line)
return raw.trimEnd().split("\n").length;
}
async function readJsonl(p: string): Promise<any[]> {
const raw = await fs.readFile(p, "utf8");
if (!raw.trim()) return [];
return raw
.trimEnd()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line));
}
function formatContactLine(c: ContactIndexRow) {
const company = c.company ? ` (${c.company})` : "";
const lastAt = c.lastMessageAt ? new Date(c.lastMessageAt).toLocaleString("ru-RU") : "нет";
return `- ${c.name}${company} · последнее: ${lastAt}`;
}
export async function runCrmAgent(userText: string): Promise<AgentReply> {
throw new Error("runCrmAgent now requires auth context");
}
export async function runCrmAgentFor(
input: {
teamId: string;
userId: string;
userText: string;
requestId?: string;
conversationId?: string;
onTrace?: (event: AgentTraceEvent) => Promise<void> | void;
},
): Promise<AgentReply> {
const mode = (process.env.CF_AGENT_MODE ?? "langgraph").toLowerCase();
const llmApiKey =
process.env.OPENROUTER_API_KEY ||
process.env.LLM_API_KEY ||
process.env.OPENAI_API_KEY ||
process.env.DASHSCOPE_API_KEY ||
process.env.QWEN_API_KEY;
const hasGigachat = Boolean((process.env.GIGACHAT_AUTH_KEY ?? "").trim() && (process.env.GIGACHAT_SCOPE ?? "").trim());
if (mode !== "rule") {
return runLangGraphCrmAgentFor(input);
}
if (!llmApiKey && !hasGigachat) {
throw new Error("LLM API key is not configured. Set OPENROUTER_API_KEY or GIGACHAT_AUTH_KEY/GIGACHAT_SCOPE.");
}
await ensureDataset({ teamId: input.teamId, userId: input.userId });
const q = normalize(input.userText);
const root = datasetRoot({ teamId: input.teamId, userId: input.userId });
const contacts = await readContactIndexFrom(root);
// "10 лучших клиентов"
if (q.includes("10 лучших") || (q.includes("топ") && q.includes("клиент"))) {
const ranked = await Promise.all(
contacts.map(async (c) => {
const msgPath = path.join(root, "messages", `${c.id}.jsonl`);
const evPath = path.join(root, "events", `${c.id}.jsonl`);
const msgCount = await countJsonlLines(msgPath).catch(() => 0);
const ev = await readJsonl(evPath).catch(() => []);
const todayEvCount = ev.filter((e) => (e?.startsAt ? isToday(new Date(e.startsAt)) : false)).length;
const score = msgCount * 2 + todayEvCount * 3;
return { c, score };
}),
);
ranked.sort((a, b) => b.score - a.score);
const top = ranked.slice(0, 10).map((x) => x.c);
return {
plan: [
"Загрузить индекс контактов из файлового датасета",
"Посчитать активность по JSONL (сообщения/события сегодня)",
"Отсортировать и показать топ",
],
tools: ["read index/contacts.json", "read messages/{contactId}.jsonl", "read events/{contactId}.jsonl"],
toolRuns: [
{
name: "dataset:index_contacts",
status: "ok",
input: "index/contacts.json",
output: "Loaded contacts index",
at: new Date().toISOString(),
},
],
text:
`Топ-10 по активности (сообщения + события):\n` +
top.map(formatContactLine).join("\n") +
`\n\nЕсли хочешь, скажи критерий "лучший" (выручка/стадия/вероятность/давность) и я пересчитаю.`,
};
}
// "чем заняться сегодня"
if (q.includes("чем") && (q.includes("сегодня") || q.includes("заняться"))) {
const todayEvents: Array<{ who: string; title: string; at: Date; note?: string | null }> = [];
for (const c of contacts) {
const evPath = path.join(root, "events", `${c.id}.jsonl`);
const ev = await readJsonl(evPath).catch(() => []);
for (const e of ev) {
if (!e?.startsAt) continue;
const at = new Date(e.startsAt);
if (!isToday(at)) continue;
todayEvents.push({ who: c.name, title: e.title ?? "Event", at, note: e.note ?? null });
}
}
todayEvents.sort((a, b) => a.at.getTime() - b.at.getTime());
const followups = [...contacts]
.map((c) => ({ c, last: c.lastMessageAt ? new Date(c.lastMessageAt).getTime() : 0 }))
.sort((a, b) => a.last - b.last)
.slice(0, 3)
.map((x) => x.c);
const lines: string[] = [];
if (todayEvents.length > 0) {
lines.push("Сегодня по календарю:");
for (const e of todayEvents) {
const hhmm = e.at.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
lines.push(`- ${hhmm} · ${e.title} · ${e.who}${e.note ? ` · ${e.note}` : ""}`);
}
} else {
lines.push("Сегодня нет запланированных событий в календаре.");
}
lines.push("");
lines.push("Фокус дня (если нужно добить прогресс):");
for (const c of followups) {
lines.push(`- Написать follow-up: ${c.name}${c.company ? ` (${c.company})` : ""}`);
}
return {
plan: [
"Прочитать события на сегодня из файлового датасета",
"Найти контакты без свежего касания (по lastMessageAt)",
"Сформировать короткий список действий",
],
tools: ["read index/contacts.json", "read events/{contactId}.jsonl"],
toolRuns: [
{
name: "dataset:query_events",
status: "ok",
input: "events/*.jsonl (today)",
output: `Found ${todayEvents.length} events`,
at: new Date().toISOString(),
},
],
text: lines.join("\n"),
};
}
throw new Error(
"Rule mode supports only structured built-in queries. Use a supported query or switch to langgraph mode with a configured LLM API key.",
);
}
export async function persistChatMessage(input: {
role: ChatRole;
text: string;
plan?: string[];
tools?: string[];
thinking?: string[];
toolRuns?: Array<{
name: string;
status: "ok" | "error";
input: string;
output: string;
at: string;
}>;
changeSet?: ChangeSet | null;
requestId?: string;
eventType?: "user" | "trace" | "assistant" | "note";
phase?: "pending" | "running" | "final" | "error";
transient?: boolean;
messageKind?: "change_set_summary";
teamId: string;
conversationId: string;
authorUserId?: string | null;
}) {
const hasStoredPayload = Boolean(input.changeSet || input.messageKind);
const data: Prisma.ChatMessageCreateInput = {
team: { connect: { id: input.teamId } },
conversation: { connect: { id: input.conversationId } },
authorUser: input.authorUserId ? { connect: { id: input.authorUserId } } : undefined,
role: input.role,
text: input.text,
planJson: hasStoredPayload
? ({
messageKind: input.messageKind ?? null,
changeSet: input.changeSet ?? null,
} as any)
: undefined,
};
return prisma.chatMessage.create({ data });
}

File diff suppressed because it is too large Load Diff