|
@@ -22,9 +22,10 @@ import {
|
|
|
createInvitation, getAllInvitations, getInvitationByToken, updateInvitationStatus,
|
|
createInvitation, getAllInvitations, getInvitationByToken, updateInvitationStatus,
|
|
|
expireOldInvitations, getInvitationByEmail,
|
|
expireOldInvitations, getInvitationByEmail,
|
|
|
createAuditLog, getAuditLogs,
|
|
createAuditLog, getAuditLogs,
|
|
|
- trackAnalyticsEvent, getAnalyticsEvents, getAnalyticsSummary,
|
|
|
|
|
|
|
+ trackAnalyticsEvent, getAnalyticsEvents, getAnalyticsSummary, getIntentStats, getResponseTimeStats,
|
|
|
createDataSource, getDataSources, getDataSourceById, updateDataSource, deleteDataSource,
|
|
createDataSource, getDataSources, getDataSourceById, updateDataSource, deleteDataSource,
|
|
|
createApiConnection, getApiConnections, getApiConnectionById, updateApiConnection, deleteApiConnection, incrementApiConnectionExecution,
|
|
createApiConnection, getApiConnections, getApiConnectionById, updateApiConnection, deleteApiConnection, incrementApiConnectionExecution,
|
|
|
|
|
+ rateConversation,
|
|
|
searchKnowledge, incrementKnowledgeUseCount, logKnowledgeSuggestion,
|
|
searchKnowledge, incrementKnowledgeUseCount, logKnowledgeSuggestion,
|
|
|
getKnowledgeEntries, getKnowledgeEntryById, createKnowledgeEntry,
|
|
getKnowledgeEntries, getKnowledgeEntryById, createKnowledgeEntry,
|
|
|
updateKnowledgeEntry, deleteKnowledgeEntry, bulkCreateKnowledgeEntries,
|
|
updateKnowledgeEntry, deleteKnowledgeEntry, bulkCreateKnowledgeEntries,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
getKnowledgeProducts, bulkCreateKnowledgeProducts, deleteAllKnowledgeProducts,
|
|
getKnowledgeProducts, bulkCreateKnowledgeProducts, deleteAllKnowledgeProducts,
|
|
|
} from "./db";
|
|
} from "./db";
|
|
|
import { detectFlowIntent, executeFlow } from "./flowEngine";
|
|
import { detectFlowIntent, executeFlow } from "./flowEngine";
|
|
|
|
|
+import { runApiConnection } from "./apiConnectionRunner";
|
|
|
import { messages } from "../drizzle/schema";
|
|
import { messages } from "../drizzle/schema";
|
|
|
import { eq, desc } from "drizzle-orm";
|
|
import { eq, desc } from "drizzle-orm";
|
|
|
import { sdk } from "./_core/sdk";
|
|
import { sdk } from "./_core/sdk";
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
lookupCatalog,
|
|
lookupCatalog,
|
|
|
lookupStock,
|
|
lookupStock,
|
|
|
lookupContact,
|
|
lookupContact,
|
|
|
|
|
+ lookupProductImages,
|
|
|
} from "./erpTools";
|
|
} from "./erpTools";
|
|
|
|
|
|
|
|
/* ─── Homelegance chatbot system prompt ─── */
|
|
/* ─── Homelegance chatbot system prompt ─── */
|
|
@@ -413,7 +416,15 @@ export const appRouter = router({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 7. Customer / dealer lookup (admin/agent only — dealers see their own record via CID)
|
|
|
|
|
|
|
+ // 7. Product images — "show image/photo/picture of [model]" or "image for 1436W-6"
|
|
|
|
|
+ if (!erpContext && /\b(image|images|photo|photos|picture|pictures|what does .* look like|show me)\b/.test(msgLower)) {
|
|
|
|
|
+ const modelMatch = msg.match(/\b([A-Z]{1,5}[-]?\d{3,}[-\w]*)\b/);
|
|
|
|
|
+ if (modelMatch) {
|
|
|
|
|
+ erpContext = await lookupProductImages(modelMatch[1], userCtx);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 8. Customer / dealer lookup (admin/agent only — dealers see their own record via CID)
|
|
|
if (!erpContext && ctx.user?.role !== "user" && /\b(customer|dealer|account|contact|company)\b/.test(msgLower)) {
|
|
if (!erpContext && ctx.user?.role !== "user" && /\b(customer|dealer|account|contact|company)\b/.test(msgLower)) {
|
|
|
const nameMatch = msg.match(/(?:customer|dealer|account|contact|company)[:\s]+([A-Za-z &'.-]{3,40})/i);
|
|
const nameMatch = msg.match(/(?:customer|dealer|account|contact|company)[:\s]+([A-Za-z &'.-]{3,40})/i);
|
|
|
if (nameMatch) {
|
|
if (nameMatch) {
|
|
@@ -472,6 +483,15 @@ export const appRouter = router({
|
|
|
// Auto-log as suggestion for continuous improvement
|
|
// Auto-log as suggestion for continuous improvement
|
|
|
logKnowledgeSuggestion(input.content).catch(() => {});
|
|
logKnowledgeSuggestion(input.content).catch(() => {});
|
|
|
|
|
|
|
|
|
|
+ // Log intent for hot-topic analytics (fire-and-forget)
|
|
|
|
|
+ trackAnalyticsEvent({
|
|
|
|
|
+ conversationId: conversation.id,
|
|
|
|
|
+ sessionId: input.sessionId,
|
|
|
|
|
+ eventType: "intent_detected",
|
|
|
|
|
+ category: "unclassified",
|
|
|
|
|
+ metadata: { snippet: input.content.slice(0, 120) },
|
|
|
|
|
+ }).catch(() => {});
|
|
|
|
|
+
|
|
|
return { reply: botReply, status: conversation.status, source: "llm" as const };
|
|
return { reply: botReply, status: conversation.status, source: "llm" as const };
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error("[Chat] LLM error:", error);
|
|
console.error("[Chat] LLM error:", error);
|
|
@@ -485,13 +505,23 @@ export const appRouter = router({
|
|
|
}
|
|
}
|
|
|
}),
|
|
}),
|
|
|
|
|
|
|
|
|
|
+ rateSatisfaction: publicProcedure
|
|
|
|
|
+ .input(z.object({
|
|
|
|
|
+ sessionId: z.string(),
|
|
|
|
|
+ rating: z.number().int().min(1).max(5),
|
|
|
|
|
+ comment: z.string().max(500).optional(),
|
|
|
|
|
+ }))
|
|
|
|
|
+ .mutation(async ({ input }) => {
|
|
|
|
|
+ return rateConversation(input.sessionId, input.rating, input.comment);
|
|
|
|
|
+ }),
|
|
|
|
|
+
|
|
|
getMessages: publicProcedure
|
|
getMessages: publicProcedure
|
|
|
.input(z.object({ sessionId: z.string() }))
|
|
.input(z.object({ sessionId: z.string() }))
|
|
|
.query(async ({ input }) => {
|
|
.query(async ({ input }) => {
|
|
|
const conversation = await getConversationBySessionId(input.sessionId);
|
|
const conversation = await getConversationBySessionId(input.sessionId);
|
|
|
if (!conversation) return { messages: [], status: "closed" as const };
|
|
if (!conversation) return { messages: [], status: "closed" as const };
|
|
|
const msgs = await getMessagesByConversation(conversation.id);
|
|
const msgs = await getMessagesByConversation(conversation.id);
|
|
|
- return { messages: msgs, status: conversation.status };
|
|
|
|
|
|
|
+ return { messages: msgs, status: conversation.status, csatRating: conversation.csatRating };
|
|
|
}),
|
|
}),
|
|
|
}),
|
|
}),
|
|
|
|
|
|
|
@@ -1195,6 +1225,24 @@ Return ONLY the JSON array, no markdown or explanation.`,
|
|
|
endDate: input?.endDate ? new Date(input.endDate) : undefined,
|
|
endDate: input?.endDate ? new Date(input.endDate) : undefined,
|
|
|
});
|
|
});
|
|
|
}),
|
|
}),
|
|
|
|
|
+
|
|
|
|
|
+ intentStats: agentProcedure
|
|
|
|
|
+ .input(z.object({ startDate: z.string().optional(), endDate: z.string().optional() }).optional())
|
|
|
|
|
+ .query(async ({ input }) => {
|
|
|
|
|
+ return getIntentStats(
|
|
|
|
|
+ input?.startDate ? new Date(input.startDate) : undefined,
|
|
|
|
|
+ input?.endDate ? new Date(input.endDate) : undefined,
|
|
|
|
|
+ );
|
|
|
|
|
+ }),
|
|
|
|
|
+
|
|
|
|
|
+ responseTime: agentProcedure
|
|
|
|
|
+ .input(z.object({ startDate: z.string().optional(), endDate: z.string().optional() }).optional())
|
|
|
|
|
+ .query(async ({ input }) => {
|
|
|
|
|
+ return getResponseTimeStats(
|
|
|
|
|
+ input?.startDate ? new Date(input.startDate) : undefined,
|
|
|
|
|
+ input?.endDate ? new Date(input.endDate) : undefined,
|
|
|
|
|
+ );
|
|
|
|
|
+ }),
|
|
|
}),
|
|
}),
|
|
|
|
|
|
|
|
/* ─── Data Sources Router (Lyro-inspired) ─── */
|
|
/* ─── Data Sources Router (Lyro-inspired) ─── */
|
|
@@ -1309,17 +1357,21 @@ Return ONLY the JSON array, no markdown or explanation.`,
|
|
|
.mutation(async ({ input }) => {
|
|
.mutation(async ({ input }) => {
|
|
|
const conn = await getApiConnectionById(input.id);
|
|
const conn = await getApiConnectionById(input.id);
|
|
|
if (!conn) throw new TRPCError({ code: "NOT_FOUND", message: "API connection not found" });
|
|
if (!conn) throw new TRPCError({ code: "NOT_FOUND", message: "API connection not found" });
|
|
|
- try {
|
|
|
|
|
- // Simulate a test call (in production, this would make the actual HTTP request)
|
|
|
|
|
- await incrementApiConnectionExecution(input.id);
|
|
|
|
|
- return {
|
|
|
|
|
- success: true,
|
|
|
|
|
- message: `Test successful for ${conn.name}`,
|
|
|
|
|
- responseTime: Math.floor(Math.random() * 500) + 100, // Simulated
|
|
|
|
|
- };
|
|
|
|
|
- } catch (err: any) {
|
|
|
|
|
- return { success: false, message: err.message, responseTime: 0 };
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const start = Date.now();
|
|
|
|
|
+ const result = await runApiConnection(input.id);
|
|
|
|
|
+ return {
|
|
|
|
|
+ success: result.success,
|
|
|
|
|
+ message: result.success ? `Test successful for ${conn.name}` : (result.error ?? "Unknown error"),
|
|
|
|
|
+ responseTime: Date.now() - start,
|
|
|
|
|
+ };
|
|
|
|
|
+ }),
|
|
|
|
|
+
|
|
|
|
|
+ run: adminProcedure
|
|
|
|
|
+ .input(z.object({ id: z.number() }))
|
|
|
|
|
+ .mutation(async ({ input }) => {
|
|
|
|
|
+ const result = await runApiConnection(input.id);
|
|
|
|
|
+ if (!result.success) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: result.error });
|
|
|
|
|
+ return result;
|
|
|
}),
|
|
}),
|
|
|
}),
|
|
}),
|
|
|
|
|
|
|
@@ -1425,6 +1477,42 @@ Return ONLY the JSON array, no markdown or explanation.`,
|
|
|
return bulkCreateKnowledgeProducts(input.products);
|
|
return bulkCreateKnowledgeProducts(input.products);
|
|
|
}),
|
|
}),
|
|
|
}),
|
|
}),
|
|
|
|
|
+
|
|
|
|
|
+ /* ─── Playground Router ─── */
|
|
|
|
|
+ playground: router({
|
|
|
|
|
+ chat: agentProcedure
|
|
|
|
|
+ .input(z.object({
|
|
|
|
|
+ sessionId: z.string().optional(),
|
|
|
|
|
+ content: z.string().min(1).max(4000),
|
|
|
|
|
+ systemPrompt: z.string().max(8000).optional(),
|
|
|
|
|
+ model: z.enum([
|
|
|
|
|
+ "claude-haiku-4-5-20251001",
|
|
|
|
|
+ "claude-sonnet-4-6",
|
|
|
|
|
+ "claude-opus-4-7",
|
|
|
|
|
+ ]).default("claude-sonnet-4-6"),
|
|
|
|
|
+ temperature: z.number().min(0).max(1).optional(),
|
|
|
|
|
+ history: z.array(z.object({
|
|
|
|
|
+ role: z.enum(["user", "assistant"]),
|
|
|
|
|
+ content: z.string(),
|
|
|
|
|
+ })).default([]),
|
|
|
|
|
+ }))
|
|
|
|
|
+ .mutation(async ({ input }) => {
|
|
|
|
|
+ const systemContent = input.systemPrompt ?? SYSTEM_PROMPT;
|
|
|
|
|
+ const llmMessages = [
|
|
|
|
|
+ { role: "system" as const, content: systemContent },
|
|
|
|
|
+ ...input.history,
|
|
|
|
|
+ { role: "user" as const, content: input.content },
|
|
|
|
|
+ ];
|
|
|
|
|
+ const result = await invokeLLM({
|
|
|
|
|
+ messages: llmMessages,
|
|
|
|
|
+ model: input.model,
|
|
|
|
|
+ ...(input.temperature != null ? { temperature: input.temperature } : {}),
|
|
|
|
|
+ });
|
|
|
|
|
+ const reply = result.choices[0]?.message?.content as string
|
|
|
|
|
+ ?? "Sorry, I could not generate a response.";
|
|
|
|
|
+ return { reply, model: input.model };
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
export type AppRouter = typeof appRouter;
|
|
export type AppRouter = typeof appRouter;
|