91 lines
4.0 KiB
TypeScript
91 lines
4.0 KiB
TypeScript
import { recordSchema, type DynamicTool, type FunctionTool, type Profile, type ServerConfig } from "./contracts";
|
|
import type { Settings } from "./settings";
|
|
|
|
export function profileName(profile: Profile | null): string {
|
|
if (!profile) {
|
|
return "";
|
|
}
|
|
for (const key of ["name", "fullName", "full_name", "displayName", "display_name", "username"]) {
|
|
const value = profile[key];
|
|
if (typeof value === "string" && value.trim()) {
|
|
return value.trim();
|
|
}
|
|
}
|
|
const full = [profile.firstName, profile.lastName]
|
|
.filter((part): part is string => typeof part === "string")
|
|
.join(" ")
|
|
.trim();
|
|
if (full) {
|
|
return full;
|
|
}
|
|
for (const key of ["user", "customer", "profile", "contact"]) {
|
|
const nested = recordSchema.safeParse(profile[key]);
|
|
if (nested.success) {
|
|
const name = profileName(nested.data);
|
|
if (name) {
|
|
return name;
|
|
}
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
export function initials(name: string) {
|
|
const words = name.trim().split(/\s+/);
|
|
return (words.length > 1 ? `${words[0]?.[0] ?? ""}${words.at(-1)?.[0] ?? ""}` : name.slice(0, 2)).toUpperCase();
|
|
}
|
|
export function profileContext(profile: Profile | null) {
|
|
if (!profile) {
|
|
return "";
|
|
}
|
|
const serialized = JSON.stringify(profile);
|
|
return `Caller profile resolved from the page phoneNumber: ${serialized.length > 1600 ? `${serialized.slice(0, 1600)}...` : serialized}. Use this as trusted caller context when relevant.`;
|
|
}
|
|
export function ragContext(text: string) {
|
|
return text.trim() ? `<<<CUSTOM_RAG_BEGIN>>>\n${text.trim()}\n<<<CUSTOM_RAG_END>>>` : "";
|
|
}
|
|
export const TOOL_MARKER = "<<<APIPILOT_TOOLS_LIST_BEGIN>>>";
|
|
export function toolContext(tools: DynamicTool[]) {
|
|
if (!tools.length) {
|
|
return "";
|
|
}
|
|
const entries = tools.slice(0, 100).map((tool) => {
|
|
const required = Array.isArray(tool.parameters.required)
|
|
? tool.parameters.required.filter((key): key is string => typeof key === "string")
|
|
: [];
|
|
return `- name: ${tool.name}\n requires_auth: ${String(tool.requires_auth)}${required.length ? `\n required_parameters: [${required.slice(0, 40).join(", ")}]` : ""}\n description: ${tool.description.slice(0, 500)}`;
|
|
});
|
|
return `${TOOL_MARKER}\n### Available dynamic tools (ApiPilot AquaPortal)\nEach tool name is a unique identifier: DO NOT change, camelCase or translate it.\nMANDATORY EXECUTION RULES:\n1. If the user request matches a tool description, output ONLY one line as the FIRST line of your response:\n<<<APIPILOT_CALL:NAME=tool_name,ARGUMENTS={"parameter1":"value1"}>>>\n2. ARGUMENTS must be valid JSON. Include ONLY fields listed in required_parameters, if any.\n3. Do NOT add text after the marker. The engine executes the tool and supplies its result in a second call.\n4. If no description matches, respond normally in Spanish without a marker.\n5. If multiple descriptions match, choose the most specific.\n${entries.join("\n")}\n<<<APIPILOT_TOOLS_LIST_END>>>`;
|
|
}
|
|
export function buildInstructions(
|
|
config: ServerConfig,
|
|
settings: Settings,
|
|
profile: Profile | null,
|
|
rag: string,
|
|
tools: DynamicTool[],
|
|
) {
|
|
const base = config.primaryPrompt || settings.instructions;
|
|
return [
|
|
base,
|
|
config.injectProfile ? profileContext(profile) : "",
|
|
ragContext(rag),
|
|
base.includes(TOOL_MARKER) ? "" : toolContext(tools),
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n\n");
|
|
}
|
|
export function speechInstructions(instructions: string, config: ServerConfig) {
|
|
return config.reasoningType === "OPENAI"
|
|
? `${instructions}\nDo not answer ordinary user speech directly. Stay silent unless the latest user message starts with [VOCERO_RENDER_REPLY]. When it does, speak only the content after the marker naturally. Do not call tools while rendering that message.`
|
|
: instructions;
|
|
}
|
|
export const WEB_SEARCH: FunctionTool = {
|
|
type: "function",
|
|
name: "web_search",
|
|
description: "Search the web for current or factual information. Returns results with titles, snippets and URLs.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: { query: { type: "string", description: "The search query." } },
|
|
required: ["query"],
|
|
},
|
|
};
|